@commonpub/editor 0.5.0 → 0.6.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.
Files changed (36) hide show
  1. package/README.md +43 -10
  2. package/package.json +16 -4
  3. package/vue/components/BlockCanvas.vue +512 -0
  4. package/vue/components/BlockInsertZone.vue +86 -0
  5. package/vue/components/BlockPicker.vue +274 -0
  6. package/vue/components/BlockWrapper.vue +188 -0
  7. package/vue/components/EditorBlocks.vue +235 -0
  8. package/vue/components/EditorSection.vue +81 -0
  9. package/vue/components/EditorShell.vue +198 -0
  10. package/vue/components/EditorTagInput.vue +116 -0
  11. package/vue/components/EditorVisibility.vue +110 -0
  12. package/vue/components/blocks/BuildStepBlock.vue +103 -0
  13. package/vue/components/blocks/CalloutBlock.vue +123 -0
  14. package/vue/components/blocks/CheckpointBlock.vue +29 -0
  15. package/vue/components/blocks/CodeBlock.vue +178 -0
  16. package/vue/components/blocks/DividerBlock.vue +22 -0
  17. package/vue/components/blocks/DownloadsBlock.vue +43 -0
  18. package/vue/components/blocks/EmbedBlock.vue +22 -0
  19. package/vue/components/blocks/GalleryBlock.vue +235 -0
  20. package/vue/components/blocks/HeadingBlock.vue +97 -0
  21. package/vue/components/blocks/ImageBlock.vue +272 -0
  22. package/vue/components/blocks/MarkdownBlock.vue +259 -0
  23. package/vue/components/blocks/MathBlock.vue +39 -0
  24. package/vue/components/blocks/PartsListBlock.vue +354 -0
  25. package/vue/components/blocks/QuizBlock.vue +49 -0
  26. package/vue/components/blocks/QuoteBlock.vue +102 -0
  27. package/vue/components/blocks/SectionHeaderBlock.vue +132 -0
  28. package/vue/components/blocks/SliderBlock.vue +320 -0
  29. package/vue/components/blocks/TextBlock.vue +202 -0
  30. package/vue/components/blocks/ToolListBlock.vue +71 -0
  31. package/vue/components/blocks/VideoBlock.vue +24 -0
  32. package/vue/composables/useBlockEditor.ts +190 -0
  33. package/vue/index.ts +59 -0
  34. package/vue/provide.ts +28 -0
  35. package/vue/types.ts +40 -0
  36. package/vue/utils.ts +12 -0
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+
4
+ const props = defineProps<{ content: Record<string, unknown> }>();
5
+ const emit = defineEmits<{ update: [content: Record<string, unknown>] }>();
6
+ interface Option { text: string; correct: boolean; }
7
+ const question = computed(() => (props.content.question as string) ?? '');
8
+ const options = computed(() => (props.content.options as Option[]) ?? []);
9
+ function updateQuestion(value: string): void { emit('update', { ...props.content, question: value }); }
10
+ function addOption(): void { emit('update', { ...props.content, options: [...options.value, { text: '', correct: false }] }); }
11
+ function updateOption(i: number, field: string, value: unknown): void {
12
+ const updated = [...options.value];
13
+ updated[i] = { ...updated[i]!, [field]: value };
14
+ emit('update', { ...props.content, options: updated });
15
+ }
16
+ function removeOption(i: number): void {
17
+ emit('update', { ...props.content, options: options.value.filter((_: Option, idx: number) => idx !== i) });
18
+ }
19
+ </script>
20
+ <template>
21
+ <div class="cpub-quiz-block">
22
+ <div class="cpub-quiz-header"><i class="fa-solid fa-circle-question"></i> Quiz</div>
23
+ <div class="cpub-quiz-body">
24
+ <input class="cpub-quiz-question" type="text" :value="question" placeholder="Ask a question..." @input="updateQuestion(($event.target as HTMLInputElement).value)" />
25
+ <div v-for="(opt, i) in options" :key="i" class="cpub-quiz-option">
26
+ <input type="checkbox" :checked="opt.correct" @change="updateOption(i, 'correct', ($event.target as HTMLInputElement).checked)" />
27
+ <input class="cpub-quiz-opt-text" type="text" :value="opt.text" placeholder="Option text..." @input="updateOption(i, 'text', ($event.target as HTMLInputElement).value)" />
28
+ <button class="cpub-quiz-opt-remove" @click="removeOption(i)"><i class="fa-solid fa-xmark"></i></button>
29
+ </div>
30
+ <button class="cpub-quiz-add-opt" @click="addOption"><i class="fa-solid fa-plus"></i> Add option</button>
31
+ </div>
32
+ </div>
33
+ </template>
34
+ <style scoped>
35
+ .cpub-quiz-block { border: var(--border-width-default) solid var(--border2); background: var(--surface); }
36
+ .cpub-quiz-header { padding: 8px 12px; font-size: 12px; font-weight: 600; background: var(--surface2); border-bottom: var(--border-width-default) solid var(--border2); display: flex; align-items: center; gap: 8px; }
37
+ .cpub-quiz-header i { color: var(--purple); }
38
+ .cpub-quiz-body { padding: 12px; }
39
+ .cpub-quiz-question { width: 100%; font-size: 14px; font-weight: 600; background: transparent; border: none; border-bottom: var(--border-width-default) solid var(--border2); padding: 6px 0; outline: none; color: var(--text); margin-bottom: 12px; }
40
+ .cpub-quiz-question::placeholder { color: var(--text-faint); }
41
+ .cpub-quiz-option { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
42
+ .cpub-quiz-option input[type="checkbox"] { accent-color: var(--green); }
43
+ .cpub-quiz-opt-text { flex: 1; font-size: 12px; background: transparent; border: none; outline: none; color: var(--text); padding: 4px; }
44
+ .cpub-quiz-opt-text::placeholder { color: var(--text-faint); }
45
+ .cpub-quiz-opt-remove { background: none; border: none; color: var(--text-faint); cursor: pointer; }
46
+ .cpub-quiz-opt-remove:hover { color: var(--red); }
47
+ .cpub-quiz-add-opt { margin-top: 8px; font-size: 11px; color: var(--text-dim); background: none; border: 2px dashed var(--border2); padding: 6px 12px; cursor: pointer; width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px; }
48
+ .cpub-quiz-add-opt:hover { border-color: var(--accent); color: var(--accent); }
49
+ </style>
@@ -0,0 +1,102 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Blockquote block — styled quote with editable body and attribution.
4
+ */
5
+ import { computed } from 'vue';
6
+ import { sanitizeBlockHtml } from '../../utils.js';
7
+
8
+ const props = defineProps<{
9
+ content: Record<string, unknown>;
10
+ }>();
11
+
12
+ const emit = defineEmits<{
13
+ update: [content: Record<string, unknown>];
14
+ }>();
15
+
16
+ const html = computed(() => (props.content.html as string) ?? '');
17
+ const attribution = computed(() => (props.content.attribution as string) ?? '');
18
+
19
+ function onBodyInput(event: Event): void {
20
+ const el = event.target as HTMLElement;
21
+ emit('update', { html: sanitizeBlockHtml(el.innerHTML), attribution: attribution.value });
22
+ }
23
+
24
+ function onAttributionInput(event: Event): void {
25
+ emit('update', { html: html.value, attribution: (event.target as HTMLInputElement).value });
26
+ }
27
+ </script>
28
+
29
+ <template>
30
+ <div class="cpub-quote-block">
31
+ <div class="cpub-quote-bar" aria-hidden="true" />
32
+ <div class="cpub-quote-body">
33
+ <div
34
+ class="cpub-quote-text"
35
+ contenteditable="true"
36
+ data-placeholder="Enter quote..."
37
+ @input="onBodyInput"
38
+ v-html="html"
39
+ />
40
+ <input
41
+ class="cpub-quote-attribution"
42
+ type="text"
43
+ :value="attribution"
44
+ placeholder="— Source or author"
45
+ aria-label="Quote attribution"
46
+ @input="onAttributionInput"
47
+ />
48
+ </div>
49
+ </div>
50
+ </template>
51
+
52
+ <style scoped>
53
+ .cpub-quote-block {
54
+ display: flex;
55
+ gap: 0;
56
+ background: var(--surface2);
57
+ border: var(--border-width-default) solid var(--border2);
58
+ }
59
+
60
+ .cpub-quote-bar {
61
+ width: 5px;
62
+ background: var(--accent);
63
+ flex-shrink: 0;
64
+ }
65
+
66
+ .cpub-quote-body {
67
+ flex: 1;
68
+ padding: 16px 20px;
69
+ }
70
+
71
+ .cpub-quote-text {
72
+ font-size: 16px;
73
+ font-style: italic;
74
+ line-height: 1.7;
75
+ color: var(--text);
76
+ outline: none;
77
+ min-height: 1.7em;
78
+ }
79
+
80
+ .cpub-quote-text:empty::before {
81
+ content: attr(data-placeholder);
82
+ color: var(--text-faint);
83
+ pointer-events: none;
84
+ }
85
+
86
+ .cpub-quote-attribution {
87
+ display: block;
88
+ width: 100%;
89
+ margin-top: 10px;
90
+ padding: 0;
91
+ font-family: var(--font-mono);
92
+ font-size: 11px;
93
+ color: var(--text-dim);
94
+ background: transparent;
95
+ border: none;
96
+ outline: none;
97
+ }
98
+
99
+ .cpub-quote-attribution::placeholder {
100
+ color: var(--text-faint);
101
+ }
102
+ </style>
@@ -0,0 +1,132 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+
4
+ const props = defineProps<{
5
+ content: Record<string, unknown>;
6
+ }>();
7
+
8
+ const emit = defineEmits<{
9
+ update: [content: Record<string, unknown>];
10
+ }>();
11
+
12
+ const tag = computed({
13
+ get: () => (props.content.tag as string) || '',
14
+ set: (v: string) => emit('update', { ...props.content, tag: v }),
15
+ });
16
+
17
+ const title = computed({
18
+ get: () => (props.content.title as string) || '',
19
+ set: (v: string) => emit('update', { ...props.content, title: v }),
20
+ });
21
+
22
+ const body = computed({
23
+ get: () => (props.content.body as string) || '',
24
+ set: (v: string) => emit('update', { ...props.content, body: v }),
25
+ });
26
+ </script>
27
+
28
+ <template>
29
+ <div class="cpub-section-header-block">
30
+ <input
31
+ v-model="tag"
32
+ type="text"
33
+ class="cpub-shb-tag"
34
+ placeholder="§ 01 — Section Name"
35
+ aria-label="Section tag"
36
+ />
37
+ <input
38
+ v-model="title"
39
+ type="text"
40
+ class="cpub-shb-title"
41
+ placeholder="Section title"
42
+ aria-label="Section title"
43
+ />
44
+ <textarea
45
+ v-model="body"
46
+ class="cpub-shb-body"
47
+ placeholder="Brief intro for this section..."
48
+ rows="2"
49
+ aria-label="Section intro"
50
+ />
51
+ </div>
52
+ </template>
53
+
54
+ <style scoped>
55
+ .cpub-section-header-block {
56
+ padding: 28px 24px 24px;
57
+ }
58
+
59
+ .cpub-shb-tag {
60
+ font-family: var(--font-mono);
61
+ font-size: 9px;
62
+ font-weight: 700;
63
+ letter-spacing: 0.2em;
64
+ text-transform: uppercase;
65
+ color: var(--accent);
66
+ margin-bottom: 10px;
67
+ display: block;
68
+ width: 100%;
69
+ background: none;
70
+ border: var(--border-width-default) solid transparent;
71
+ padding: 2px 4px;
72
+ outline: none;
73
+ }
74
+
75
+ .cpub-shb-tag::placeholder {
76
+ color: var(--text-faint);
77
+ font-weight: 500;
78
+ }
79
+
80
+ .cpub-shb-tag:focus {
81
+ border-color: var(--accent-border);
82
+ background: var(--accent-bg);
83
+ }
84
+
85
+ .cpub-shb-title {
86
+ font-size: 26px;
87
+ font-weight: 700;
88
+ letter-spacing: -0.03em;
89
+ color: var(--text);
90
+ line-height: 1.15;
91
+ margin-bottom: 10px;
92
+ display: block;
93
+ width: 100%;
94
+ background: none;
95
+ border: var(--border-width-default) solid transparent;
96
+ padding: 4px;
97
+ outline: none;
98
+ font-family: inherit;
99
+ }
100
+
101
+ .cpub-shb-title::placeholder {
102
+ color: var(--text-faint);
103
+ }
104
+
105
+ .cpub-shb-title:focus {
106
+ border-color: var(--accent-border);
107
+ background: var(--surface2);
108
+ }
109
+
110
+ .cpub-shb-body {
111
+ font-size: 14px;
112
+ color: var(--text-dim);
113
+ line-height: 1.75;
114
+ max-width: 540px;
115
+ width: 100%;
116
+ resize: vertical;
117
+ background: none;
118
+ border: var(--border-width-default) solid transparent;
119
+ padding: 4px;
120
+ outline: none;
121
+ font-family: inherit;
122
+ }
123
+
124
+ .cpub-shb-body::placeholder {
125
+ color: var(--text-faint);
126
+ }
127
+
128
+ .cpub-shb-body:focus {
129
+ border-color: var(--accent-border);
130
+ background: var(--surface2);
131
+ }
132
+ </style>
@@ -0,0 +1,320 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+
4
+ const props = defineProps<{ content: Record<string, unknown> }>();
5
+ const emit = defineEmits<{ update: [content: Record<string, unknown>] }>();
6
+
7
+ interface FeedbackRange {
8
+ min: number;
9
+ max: number;
10
+ state: string;
11
+ message: string;
12
+ }
13
+
14
+ const label = computed(() => (props.content.label as string) ?? '');
15
+ const min = computed(() => (props.content.min as number) ?? 0);
16
+ const max = computed(() => (props.content.max as number) ?? 100);
17
+ const step = computed(() => (props.content.step as number) ?? 1);
18
+ const unit = computed(() => (props.content.unit as string) ?? '');
19
+ const defaultValue = computed(() => (props.content.defaultValue as number) ?? Math.round((min.value + max.value) / 2));
20
+ const feedback = computed<FeedbackRange[]>(() => {
21
+ const raw = props.content.feedback;
22
+ if (!Array.isArray(raw)) return [];
23
+ return raw as FeedbackRange[];
24
+ });
25
+
26
+ function update(field: string, value: unknown): void {
27
+ emit('update', { ...props.content, [field]: value });
28
+ }
29
+
30
+ function addFeedbackRange(): void {
31
+ const ranges = [...feedback.value];
32
+ // Default: fill the remaining range
33
+ const lastMax = ranges.length > 0 ? ranges[ranges.length - 1]!.max + 1 : min.value;
34
+ ranges.push({
35
+ min: lastMax,
36
+ max: max.value,
37
+ state: 'ok',
38
+ message: 'Describe what this range means...',
39
+ });
40
+ update('feedback', ranges);
41
+ }
42
+
43
+ function updateFeedbackRange(index: number, field: keyof FeedbackRange, value: string | number): void {
44
+ const ranges = [...feedback.value];
45
+ const range = { ...ranges[index]! };
46
+ if (field === 'min' || field === 'max') {
47
+ range[field] = Number(value);
48
+ } else {
49
+ range[field] = value as string;
50
+ }
51
+ ranges[index] = range;
52
+ update('feedback', ranges);
53
+ }
54
+
55
+ function removeFeedbackRange(index: number): void {
56
+ const ranges = feedback.value.filter((_: FeedbackRange, i: number) => i !== index);
57
+ update('feedback', ranges);
58
+ }
59
+
60
+ const stateOptions = [
61
+ { value: 'low', label: 'Low', color: 'var(--yellow)' },
62
+ { value: 'slow', label: 'Slow', color: 'var(--yellow)' },
63
+ { value: 'ok', label: 'OK', color: 'var(--green)' },
64
+ { value: 'good', label: 'Good', color: 'var(--green)' },
65
+ { value: 'high', label: 'High', color: 'var(--red)' },
66
+ { value: 'danger', label: 'Danger', color: 'var(--red)' },
67
+ ];
68
+
69
+ // Live preview value
70
+ const previewValue = ref(defaultValue.value);
71
+ const previewFillPct = computed(() => {
72
+ if (max.value === min.value) return 0;
73
+ return ((previewValue.value - min.value) / (max.value - min.value)) * 100;
74
+ });
75
+ const previewFeedback = computed(() => {
76
+ return feedback.value.find(
77
+ (r) => previewValue.value >= r.min && previewValue.value <= r.max,
78
+ );
79
+ });
80
+ </script>
81
+
82
+ <template>
83
+ <div class="cpub-slider-edit">
84
+ <div class="cpub-slider-edit-header"><i class="fa-solid fa-sliders"></i> Interactive Slider</div>
85
+ <div class="cpub-slider-edit-body">
86
+ <label class="cpub-edit-label">Label</label>
87
+ <input class="cpub-edit-input" :value="label" placeholder="e.g. Learning Rate" @input="update('label', ($event.target as HTMLInputElement).value)" />
88
+
89
+ <div class="cpub-edit-row">
90
+ <div class="cpub-edit-field">
91
+ <label class="cpub-edit-label">Min</label>
92
+ <input class="cpub-edit-input" type="number" :value="min" @input="update('min', Number(($event.target as HTMLInputElement).value))" />
93
+ </div>
94
+ <div class="cpub-edit-field">
95
+ <label class="cpub-edit-label">Max</label>
96
+ <input class="cpub-edit-input" type="number" :value="max" @input="update('max', Number(($event.target as HTMLInputElement).value))" />
97
+ </div>
98
+ <div class="cpub-edit-field">
99
+ <label class="cpub-edit-label">Step</label>
100
+ <input class="cpub-edit-input" type="number" :value="step" @input="update('step', Number(($event.target as HTMLInputElement).value))" />
101
+ </div>
102
+ <div class="cpub-edit-field">
103
+ <label class="cpub-edit-label">Unit</label>
104
+ <input class="cpub-edit-input" :value="unit" placeholder="e.g. MHz" @input="update('unit', ($event.target as HTMLInputElement).value)" />
105
+ </div>
106
+ </div>
107
+
108
+ <div class="cpub-edit-field">
109
+ <label class="cpub-edit-label">Default Value</label>
110
+ <input class="cpub-edit-input" type="number" :value="defaultValue" :min="min" :max="max" @input="update('defaultValue', Number(($event.target as HTMLInputElement).value))" />
111
+ </div>
112
+
113
+ <!-- Feedback Ranges -->
114
+ <div class="cpub-feedback-section">
115
+ <div class="cpub-feedback-header">
116
+ <span class="cpub-edit-label" style="margin: 0">Feedback Ranges</span>
117
+ <span class="cpub-feedback-hint">Define what different slider positions mean</span>
118
+ </div>
119
+
120
+ <div v-if="feedback.length === 0" class="cpub-feedback-empty">
121
+ <i class="fa-solid fa-comment-dots"></i>
122
+ <span>No feedback ranges configured. The slider will show a value but no contextual message.</span>
123
+ </div>
124
+
125
+ <div
126
+ v-for="(range, i) in feedback"
127
+ :key="i"
128
+ class="cpub-feedback-range"
129
+ >
130
+ <div class="cpub-range-top-row">
131
+ <div class="cpub-range-bounds">
132
+ <input
133
+ class="cpub-range-input"
134
+ type="number"
135
+ :value="range.min"
136
+ title="Range min"
137
+ @input="updateFeedbackRange(i, 'min', ($event.target as HTMLInputElement).value)"
138
+ />
139
+ <span class="cpub-range-dash">—</span>
140
+ <input
141
+ class="cpub-range-input"
142
+ type="number"
143
+ :value="range.max"
144
+ title="Range max"
145
+ @input="updateFeedbackRange(i, 'max', ($event.target as HTMLInputElement).value)"
146
+ />
147
+ </div>
148
+ <select
149
+ class="cpub-range-state"
150
+ :value="range.state"
151
+ @change="updateFeedbackRange(i, 'state', ($event.target as HTMLSelectElement).value)"
152
+ >
153
+ <option v-for="opt in stateOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
154
+ </select>
155
+ <button class="cpub-range-remove" title="Remove range" @click="removeFeedbackRange(i)">
156
+ <i class="fa-solid fa-xmark"></i>
157
+ </button>
158
+ </div>
159
+ <input
160
+ class="cpub-edit-input cpub-range-message"
161
+ :value="range.message"
162
+ placeholder="What does this range mean? e.g. 'Too slow — model won't converge'"
163
+ @input="updateFeedbackRange(i, 'message', ($event.target as HTMLInputElement).value)"
164
+ />
165
+ </div>
166
+
167
+ <button class="cpub-feedback-add" @click="addFeedbackRange">
168
+ <i class="fa-solid fa-plus"></i> Add feedback range
169
+ </button>
170
+ </div>
171
+
172
+ <!-- Live Preview -->
173
+ <div class="cpub-slider-live-preview">
174
+ <div class="cpub-preview-label">Live Preview</div>
175
+ <div class="cpub-preview-value">{{ previewValue }}{{ unit }}</div>
176
+ <div class="cpub-preview-track-wrap">
177
+ <div class="cpub-preview-fill" :style="{ width: previewFillPct + '%' }"></div>
178
+ <input
179
+ v-model.number="previewValue"
180
+ type="range"
181
+ class="cpub-preview-range"
182
+ :min="min"
183
+ :max="max"
184
+ :step="step"
185
+ />
186
+ </div>
187
+ <div v-if="previewFeedback" class="cpub-preview-feedback" :class="`state-${previewFeedback.state}`">
188
+ {{ previewFeedback.message }}
189
+ </div>
190
+ <div v-else class="cpub-preview-feedback cpub-preview-feedback--empty">
191
+ No feedback for this value range
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </template>
197
+
198
+ <style scoped>
199
+ .cpub-slider-edit { border: var(--border-width-default) solid var(--accent-border); background: var(--surface); }
200
+ .cpub-slider-edit-header { padding: 8px 12px; font-size: 12px; font-weight: 600; background: var(--accent-bg); border-bottom: var(--border-width-default) solid var(--accent-border); display: flex; align-items: center; gap: 8px; color: var(--accent); }
201
+ .cpub-slider-edit-body { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
202
+ .cpub-edit-label { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 2px; display: block; }
203
+ .cpub-edit-input { width: 100%; font-size: 12px; background: var(--surface2); border: var(--border-width-default) solid var(--border2); padding: 6px 8px; color: var(--text); outline: none; }
204
+ .cpub-edit-input:focus { border-color: var(--accent); }
205
+ .cpub-edit-row { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 8px; }
206
+ .cpub-edit-field { display: flex; flex-direction: column; }
207
+
208
+ /* Feedback Ranges */
209
+ .cpub-feedback-section { margin-top: 4px; border-top: var(--border-width-default) solid var(--border2); padding-top: 10px; }
210
+ .cpub-feedback-header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
211
+ .cpub-feedback-hint { font-size: 10px; color: var(--text-dim); }
212
+
213
+ .cpub-feedback-empty {
214
+ padding: 10px 12px;
215
+ background: var(--surface2);
216
+ border: 2px dashed var(--border2);
217
+ font-size: 11px;
218
+ color: var(--text-dim);
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 8px;
222
+ margin-bottom: 8px;
223
+ }
224
+ .cpub-feedback-empty i { color: var(--text-faint); }
225
+
226
+ .cpub-feedback-range {
227
+ background: var(--surface2);
228
+ border: var(--border-width-default) solid var(--border2);
229
+ padding: 8px;
230
+ margin-bottom: 4px;
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 6px;
234
+ }
235
+
236
+ .cpub-range-top-row { display: flex; align-items: center; gap: 6px; }
237
+
238
+ .cpub-range-bounds { display: flex; align-items: center; gap: 4px; }
239
+ .cpub-range-input {
240
+ width: 60px; font-size: 11px; font-family: var(--font-mono);
241
+ background: var(--surface); border: var(--border-width-default) solid var(--border2);
242
+ padding: 4px 6px; color: var(--text); outline: none; text-align: center;
243
+ }
244
+ .cpub-range-input:focus { border-color: var(--accent); }
245
+ .cpub-range-dash { font-size: 10px; color: var(--text-faint); }
246
+
247
+ .cpub-range-state {
248
+ font-size: 11px; font-family: var(--font-mono);
249
+ background: var(--surface); border: var(--border-width-default) solid var(--border2);
250
+ padding: 4px 6px; color: var(--text); outline: none;
251
+ min-width: 70px;
252
+ }
253
+
254
+ .cpub-range-remove {
255
+ background: none; border: none; color: var(--text-faint);
256
+ cursor: pointer; padding: 2px 4px; font-size: 10px; margin-left: auto;
257
+ }
258
+ .cpub-range-remove:hover { color: var(--red); }
259
+
260
+ .cpub-range-message { font-size: 11px; }
261
+
262
+ .cpub-feedback-add {
263
+ width: 100%;
264
+ font-size: 11px;
265
+ color: var(--text-dim);
266
+ background: none;
267
+ border: 2px dashed var(--border2);
268
+ padding: 6px 12px;
269
+ cursor: pointer;
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ gap: 6px;
274
+ margin-top: 4px;
275
+ }
276
+ .cpub-feedback-add:hover { border-color: var(--accent); color: var(--accent); }
277
+
278
+ /* Live Preview */
279
+ .cpub-slider-live-preview {
280
+ margin-top: 4px;
281
+ border-top: var(--border-width-default) solid var(--border2);
282
+ padding-top: 10px;
283
+ }
284
+ .cpub-preview-label { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
285
+ .cpub-preview-value { font-family: var(--font-mono); font-size: 18px; font-weight: 700; color: var(--accent); margin-bottom: 8px; }
286
+
287
+ .cpub-preview-track-wrap { position: relative; margin-bottom: 6px; }
288
+ .cpub-preview-fill {
289
+ position: absolute; top: 50%; left: 0; height: 4px;
290
+ background: var(--accent); transform: translateY(-50%);
291
+ pointer-events: none; transition: width 0.05s;
292
+ }
293
+ .cpub-preview-range {
294
+ -webkit-appearance: none; appearance: none;
295
+ width: 100%; height: 4px; background: var(--surface3);
296
+ border: var(--border-width-default) solid var(--border2); outline: none; cursor: pointer;
297
+ position: relative; z-index: 1;
298
+ }
299
+ .cpub-preview-range::-webkit-slider-thumb {
300
+ -webkit-appearance: none; appearance: none;
301
+ width: 14px; height: 14px; background: var(--accent);
302
+ border: var(--border-width-default) solid var(--border); cursor: pointer;
303
+ }
304
+ .cpub-preview-range::-moz-range-thumb {
305
+ width: 14px; height: 14px; background: var(--accent);
306
+ border: var(--border-width-default) solid var(--border); cursor: pointer;
307
+ }
308
+
309
+ .cpub-preview-feedback {
310
+ font-size: 11px; padding: 6px 10px; margin-top: 4px;
311
+ display: flex; align-items: center; gap: 6px;
312
+ }
313
+ .cpub-preview-feedback.state-slow,
314
+ .cpub-preview-feedback.state-low { background: var(--yellow-bg); border: var(--border-width-default) solid var(--yellow-border); color: var(--yellow); }
315
+ .cpub-preview-feedback.state-ok,
316
+ .cpub-preview-feedback.state-good { background: var(--green-bg); border: var(--border-width-default) solid var(--green-border); color: var(--green); }
317
+ .cpub-preview-feedback.state-high,
318
+ .cpub-preview-feedback.state-danger { background: var(--red-bg); border: var(--border-width-default) solid var(--red-border); color: var(--red); }
319
+ .cpub-preview-feedback--empty { background: var(--surface2); border: 2px dashed var(--border2); color: var(--text-faint); font-style: italic; }
320
+ </style>