@open-slide/core 1.3.0 → 1.5.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.
Files changed (45) hide show
  1. package/dist/{build-_276DMmJ.js → build-DZhbjQpQ.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-D9cZ1A0X.d.ts → config-BQdTMho4.d.ts} +2 -1
  4. package/dist/{config-BAwKWNtW.js → config-iKjqaX08.js} +2528 -1640
  5. package/dist/{dev-BoqeVXVq.js → dev-BjLGk5nN.js} +1 -1
  6. package/dist/{en-CDKzoZvf.js → en-DDGqyNaW.js} +27 -4
  7. package/dist/index.d.ts +4 -2
  8. package/dist/index.js +1 -1
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +82 -13
  11. package/dist/{preview-BLPxspc9.js → preview-jwLWHWkQ.js} +1 -1
  12. package/dist/{types-JYG1cmwC.d.ts → types-Dpr8nbih.d.ts} +27 -1
  13. package/dist/vite/index.d.ts +2 -2
  14. package/dist/vite/index.js +1 -1
  15. package/package.json +1 -1
  16. package/skills/slide-authoring/SKILL.md +19 -4
  17. package/src/app/app.tsx +2 -0
  18. package/src/app/components/asset-view.tsx +111 -18
  19. package/src/app/components/inspector/inspect-overlay.tsx +49 -3
  20. package/src/app/components/inspector/inspector-panel.tsx +267 -25
  21. package/src/app/components/inspector/inspector-provider.tsx +390 -49
  22. package/src/app/components/panel/panel-shell.tsx +5 -3
  23. package/src/app/components/player.tsx +25 -5
  24. package/src/app/components/present/control-bar.tsx +12 -0
  25. package/src/app/components/present/laser-pointer.tsx +3 -4
  26. package/src/app/components/present/progress-bar.tsx +4 -4
  27. package/src/app/components/sidebar/folder-item.tsx +14 -3
  28. package/src/app/components/sidebar/sidebar.tsx +10 -0
  29. package/src/app/lib/assets.ts +21 -0
  30. package/src/app/lib/export-pdf.ts +6 -0
  31. package/src/app/lib/inspector/use-editor.ts +9 -1
  32. package/src/app/lib/sdk.ts +2 -0
  33. package/src/app/lib/slides.ts +9 -0
  34. package/src/app/lib/use-slide-module.ts +48 -0
  35. package/src/app/routes/assets.tsx +9 -0
  36. package/src/app/routes/home-shell.tsx +23 -2
  37. package/src/app/routes/home.tsx +101 -3
  38. package/src/app/routes/presenter.tsx +2 -20
  39. package/src/app/routes/slide.tsx +117 -39
  40. package/src/app/virtual.d.ts +1 -0
  41. package/src/locale/en.ts +28 -5
  42. package/src/locale/ja.ts +28 -5
  43. package/src/locale/types.ts +27 -1
  44. package/src/locale/zh-cn.ts +28 -6
  45. package/src/locale/zh-tw.ts +28 -6
@@ -34,6 +34,7 @@ import {
34
34
  } from '@/components/ui/select';
35
35
  import { Separator } from '@/components/ui/separator';
36
36
  import { Slider } from '@/components/ui/slider';
37
+ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
37
38
  import { Textarea } from '@/components/ui/textarea';
38
39
  import { Toggle } from '@/components/ui/toggle';
39
40
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
@@ -61,13 +62,41 @@ type ElementSnapshot = {
61
62
  placeholder: { hint: string; width?: number; height?: number } | null;
62
63
  };
63
64
 
65
+ type ContentSelection = { start: number; end: number };
66
+ type StylePreview = Partial<
67
+ Pick<ElementSnapshot, 'fontSize' | 'fontWeight' | 'fontStyle' | 'color'>
68
+ >;
69
+ type RangeStylePreview = {
70
+ anchor: HTMLElement;
71
+ start: number;
72
+ end: number;
73
+ values: StylePreview;
74
+ };
75
+
76
+ function resolveSelectedTarget(target: SelectedTarget, slideId: string): SelectedTarget {
77
+ const hit = findSlideSource(target.anchor, slideId, { hostOnly: true });
78
+ if (!hit) return target;
79
+ if (hit.line === target.line && hit.column === target.column && hit.anchor === target.anchor) {
80
+ return target;
81
+ }
82
+ return { line: hit.line, column: hit.column, anchor: hit.anchor };
83
+ }
84
+
64
85
  export function InspectorPanel() {
65
86
  const { active, slideId, selected, setSelected, bufferOps, pendingCount, add, applyEdit } =
66
87
  useInspector();
67
88
  const [snapshot, setSnapshot] = useState<ElementSnapshot | null>(null);
89
+ const [contentSelection, setContentSelection] = useState<ContentSelection | null>(null);
90
+ const [rangeStylePreview, setRangeStylePreview] = useState<RangeStylePreview | null>(null);
68
91
  const reloadCounter = useReloadCounter();
69
92
  const t = useLocale();
70
93
 
94
+ useEffect(() => {
95
+ void selected;
96
+ setContentSelection(null);
97
+ setRangeStylePreview(null);
98
+ }, [selected]);
99
+
71
100
  useEffect(() => {
72
101
  void reloadCounter;
73
102
  void pendingCount;
@@ -115,10 +144,12 @@ export function InspectorPanel() {
115
144
  const apply = useCallback(
116
145
  (ops: EditOp[]) => {
117
146
  if (!selected) return;
118
- bufferOps(selected.line, selected.column, selected.anchor, ops);
119
- if (selected.anchor.isConnected) setSnapshot(readSnapshot(selected.anchor));
147
+ const target = resolveSelectedTarget(selected, slideId);
148
+ if (target !== selected) setSelected(target);
149
+ bufferOps(target.line, target.column, target.anchor, ops);
150
+ if (target.anchor.isConnected) setSnapshot(readSnapshot(target.anchor));
120
151
  },
121
- [selected, bufferOps],
152
+ [selected, setSelected, slideId, bufferOps],
122
153
  );
123
154
 
124
155
  // `pinned` keeps the last selection rendered through the close-out
@@ -140,6 +171,76 @@ export function InspectorPanel() {
140
171
 
141
172
  if (!pinned) return null;
142
173
  const { s: pinSelected, n: pinSnapshot } = pinned;
174
+ const contentRange =
175
+ pinSnapshot.text !== null && contentSelection && contentSelection.end > contentSelection.start
176
+ ? contentSelection
177
+ : null;
178
+ const rangePreviewApplies =
179
+ contentRange &&
180
+ rangeStylePreview &&
181
+ rangeStylePreview.anchor === pinSelected.anchor &&
182
+ rangeStylePreview.start === contentRange.start &&
183
+ rangeStylePreview.end === contentRange.end;
184
+ const typographySnapshot = rangePreviewApplies
185
+ ? withStylePreview(pinSnapshot, rangeStylePreview.values)
186
+ : pinSnapshot;
187
+ const applyTextStyle = (ops: EditOp[]) => {
188
+ const styleOps = ops.flatMap((op) => (op.kind === 'set-style' ? [op] : []));
189
+ const target = resolveSelectedTarget(pinSelected, slideId);
190
+ if (target !== pinSelected) setSelected(target);
191
+ if (
192
+ contentRange &&
193
+ pinSnapshot.text !== null &&
194
+ styleOps.length === 1 &&
195
+ styleOps.length === ops.length &&
196
+ styleOps.every((op) => INLINE_CONTENT_STYLE_KEYS.has(op.key))
197
+ ) {
198
+ bufferOps(
199
+ target.line,
200
+ target.column,
201
+ target.anchor,
202
+ styleOps.map((op) => ({
203
+ kind: 'set-text-range-style',
204
+ start: contentRange.start,
205
+ end: contentRange.end,
206
+ key: op.key,
207
+ value: op.value,
208
+ prevText: pinSnapshot.text ?? undefined,
209
+ })),
210
+ );
211
+ setRangeStylePreview((current) => ({
212
+ anchor: target.anchor,
213
+ start: contentRange.start,
214
+ end: contentRange.end,
215
+ values: {
216
+ ...(current?.anchor === target.anchor &&
217
+ current.start === contentRange.start &&
218
+ current.end === contentRange.end
219
+ ? current.values
220
+ : {}),
221
+ ...stylePreviewFromOps(styleOps),
222
+ },
223
+ }));
224
+ if (target.anchor.isConnected) setSnapshot(readSnapshot(target.anchor));
225
+ return;
226
+ }
227
+ if (
228
+ pinSnapshot.text !== null &&
229
+ styleOps.length > 0 &&
230
+ styleOps.length === ops.length &&
231
+ styleOps.every((op) => INLINE_CONTENT_STYLE_KEYS.has(op.key))
232
+ ) {
233
+ bufferOps(
234
+ target.line,
235
+ target.column,
236
+ target.anchor,
237
+ styleOps.map((op) => ({ ...op, prevText: pinSnapshot.text ?? undefined })),
238
+ );
239
+ if (target.anchor.isConnected) setSnapshot(readSnapshot(target.anchor));
240
+ return;
241
+ }
242
+ apply(ops);
243
+ };
143
244
 
144
245
  return (
145
246
  <PanelShell
@@ -174,16 +275,20 @@ export function InspectorPanel() {
174
275
  >
175
276
  {pinSnapshot.text !== null && (
176
277
  <Section title={t.inspector.contentSection}>
177
- <ContentField snapshot={pinSnapshot} apply={apply} />
278
+ <ContentField
279
+ snapshot={pinSnapshot}
280
+ apply={apply}
281
+ onSelectionChange={setContentSelection}
282
+ />
178
283
  </Section>
179
284
  )}
180
285
 
181
286
  <Separator />
182
287
 
183
288
  <Section title={t.inspector.typographySection}>
184
- <FontSizeField snapshot={pinSnapshot} apply={apply} />
185
- <FontWeightField snapshot={pinSnapshot} apply={apply} />
186
- <StyleToggles snapshot={pinSnapshot} apply={apply} />
289
+ <FontSizeField snapshot={typographySnapshot} apply={applyTextStyle} />
290
+ <FontWeightField snapshot={typographySnapshot} apply={applyTextStyle} />
291
+ <StyleToggles snapshot={typographySnapshot} apply={applyTextStyle} />
187
292
  <LineHeightField snapshot={pinSnapshot} apply={apply} />
188
293
  <LetterSpacingField snapshot={pinSnapshot} apply={apply} />
189
294
  <TextAlignField snapshot={pinSnapshot} apply={apply} />
@@ -194,8 +299,8 @@ export function InspectorPanel() {
194
299
  <Section title={t.inspector.colorSection}>
195
300
  <ColorField
196
301
  label={t.inspector.textColor}
197
- value={pinSnapshot.color}
198
- onChange={(v) => apply([{ kind: 'set-style', key: 'color', value: v }])}
302
+ value={typographySnapshot.color}
303
+ onChange={(v) => applyTextStyle([{ kind: 'set-style', key: 'color', value: v }])}
199
304
  clearable={false}
200
305
  />
201
306
  <ColorField
@@ -254,12 +359,43 @@ const EDITING_FREEZE_CSS = `
254
359
  }
255
360
  `;
256
361
 
362
+ const INLINE_CONTENT_STYLE_KEYS = new Set([
363
+ 'fontSize',
364
+ 'fontWeight',
365
+ 'fontStyle',
366
+ 'fontFamily',
367
+ 'color',
368
+ ]);
369
+
370
+ function stylePreviewFromOps(ops: Array<Extract<EditOp, { kind: 'set-style' }>>): StylePreview {
371
+ const preview: StylePreview = {};
372
+ for (const op of ops) {
373
+ if (op.key === 'fontSize' && op.value) {
374
+ const n = parseFloat(op.value);
375
+ if (Number.isFinite(n)) preview.fontSize = n;
376
+ } else if (op.key === 'fontWeight') {
377
+ preview.fontWeight = op.value ? Number(op.value) || 400 : 400;
378
+ } else if (op.key === 'fontStyle') {
379
+ preview.fontStyle = op.value === 'italic' ? 'italic' : 'normal';
380
+ } else if (op.key === 'color' && op.value) {
381
+ preview.color = op.value;
382
+ }
383
+ }
384
+ return preview;
385
+ }
386
+
387
+ function withStylePreview(snapshot: ElementSnapshot, preview: StylePreview): ElementSnapshot {
388
+ return { ...snapshot, ...preview };
389
+ }
390
+
257
391
  function ContentField({
258
392
  snapshot,
259
393
  apply,
394
+ onSelectionChange,
260
395
  }: {
261
396
  snapshot: ElementSnapshot;
262
397
  apply: (ops: EditOp[]) => void;
398
+ onSelectionChange?: (selection: ContentSelection | null) => void;
263
399
  }) {
264
400
  // Mirror the value locally and skip syncs during IME composition;
265
401
  // a re-render mid-composition would otherwise clobber in-progress
@@ -272,6 +408,12 @@ function ContentField({
272
408
  if (!composingRef.current) setLocal(snapshot.text ?? '');
273
409
  }, [snapshot.text]);
274
410
 
411
+ const reportSelection = (el: HTMLTextAreaElement) => {
412
+ const start = el.selectionStart ?? 0;
413
+ const end = el.selectionEnd ?? start;
414
+ onSelectionChange?.(end > start ? { start, end } : null);
415
+ };
416
+
275
417
  return (
276
418
  <Textarea
277
419
  value={local}
@@ -282,17 +424,23 @@ function ContentField({
282
424
  composingRef.current = false;
283
425
  const v = e.currentTarget.value;
284
426
  setLocal(v);
427
+ reportSelection(e.currentTarget);
285
428
  apply([{ kind: 'set-text', value: v }]);
286
429
  }}
287
430
  onChange={(e) => {
288
431
  const v = e.target.value;
289
432
  setLocal(v);
433
+ reportSelection(e.currentTarget);
290
434
  if (!composingRef.current) {
291
435
  apply([{ kind: 'set-text', value: v }]);
292
436
  }
293
437
  }}
438
+ onKeyUp={(e) => reportSelection(e.currentTarget)}
439
+ onMouseUp={(e) => reportSelection(e.currentTarget)}
440
+ onSelect={(e) => reportSelection(e.currentTarget)}
441
+ wrap="off"
294
442
  rows={3}
295
- className="min-h-16 resize-none text-xs"
443
+ className="field-sizing-fixed min-h-16 w-full resize-none overflow-x-auto whitespace-pre text-xs"
296
444
  placeholder={t.inspector.elementTextPlaceholder}
297
445
  />
298
446
  );
@@ -657,13 +805,15 @@ function ImageField({
657
805
  <AssetPickerDialog
658
806
  slideId={slideId}
659
807
  onClose={() => setOpen(false)}
660
- onPick={(asset) => {
808
+ onPick={(asset, scope) => {
661
809
  setOpen(false);
810
+ const assetPath =
811
+ scope === 'global' ? `@assets/${asset.name}` : `./assets/${asset.name}`;
662
812
  const ops: EditOp[] = [
663
813
  {
664
814
  kind: 'set-attr-asset',
665
815
  attr: 'src',
666
- assetPath: `./assets/${asset.name}`,
816
+ assetPath,
667
817
  previewUrl: asset.url,
668
818
  },
669
819
  ];
@@ -722,14 +872,16 @@ function PlaceholderField({
722
872
  <AssetPickerDialog
723
873
  slideId={slideId}
724
874
  onClose={() => setOpen(false)}
725
- onPick={async (asset) => {
875
+ onPick={async (asset, scope) => {
726
876
  setOpen(false);
727
877
  setSubmitting(true);
728
878
  try {
879
+ const assetPath =
880
+ scope === 'global' ? `@assets/${asset.name}` : `./assets/${asset.name}`;
729
881
  await applyEdit(line, column, [
730
882
  {
731
883
  kind: 'replace-placeholder-with-image',
732
- assetPath: `./assets/${asset.name}`,
884
+ assetPath,
733
885
  },
734
886
  ]);
735
887
  } finally {
@@ -742,6 +894,9 @@ function PlaceholderField({
742
894
  );
743
895
  }
744
896
 
897
+ type PickerScope = 'slide' | 'global';
898
+ const GLOBAL_PICKER_SLIDE_ID = '@global';
899
+
745
900
  function AssetPickerDialog({
746
901
  slideId,
747
902
  onClose,
@@ -749,12 +904,14 @@ function AssetPickerDialog({
749
904
  }: {
750
905
  slideId: string;
751
906
  onClose: () => void;
752
- onPick: (asset: AssetEntry) => void;
907
+ onPick: (asset: AssetEntry, scope: PickerScope) => void;
753
908
  }) {
754
- const { assets, loading, refresh } = useAssets(slideId);
909
+ const [scope, setScope] = useState<PickerScope>('slide');
910
+ const effectiveSlideId = scope === 'global' ? GLOBAL_PICKER_SLIDE_ID : slideId;
911
+ const { assets, loading, refresh } = useAssets(effectiveSlideId);
755
912
  const images = assets.filter((a) => a.mime.startsWith('image/'));
756
913
  const t = useLocale();
757
- const path = `slides/${slideId}/assets/`;
914
+ const path = scope === 'global' ? 'assets/' : `slides/${slideId}/assets/`;
758
915
  const [descPrefix, descSuffix] = t.inspector.replaceImageDescription.split('{path}');
759
916
  const [uploading, setUploading] = useState(false);
760
917
  const [dragActive, setDragActive] = useState(false);
@@ -766,18 +923,18 @@ function AssetPickerDialog({
766
923
  if (!file.type.startsWith('image/')) return;
767
924
  setUploading(true);
768
925
  try {
769
- const { ok, status, entry } = await uploadWithAutoRename(slideId, file);
926
+ const { ok, status, entry } = await uploadWithAutoRename(effectiveSlideId, file);
770
927
  if (!ok || !entry) {
771
928
  toast.error(format(t.asset.toastUploadFailed, { status }));
772
929
  return;
773
930
  }
774
931
  await refresh().catch(() => {});
775
- onPick(entry);
932
+ onPick(entry, scope);
776
933
  } finally {
777
934
  setUploading(false);
778
935
  }
779
936
  },
780
- [slideId, refresh, onPick, t],
937
+ [effectiveSlideId, scope, refresh, onPick, t],
781
938
  );
782
939
 
783
940
  return (
@@ -791,6 +948,12 @@ function AssetPickerDialog({
791
948
  {descSuffix}
792
949
  </DialogDescription>
793
950
  </DialogHeader>
951
+ <Tabs value={scope} onValueChange={(next) => setScope(next as PickerScope)}>
952
+ <TabsList>
953
+ <TabsTrigger value="slide">{t.asset.scopeSlide}</TabsTrigger>
954
+ <TabsTrigger value="global">{t.asset.scopeGlobal}</TabsTrigger>
955
+ </TabsList>
956
+ </Tabs>
794
957
  <label
795
958
  htmlFor={inputId}
796
959
  className={cn(
@@ -859,7 +1022,7 @@ function AssetPickerDialog({
859
1022
  <button
860
1023
  key={asset.name}
861
1024
  type="button"
862
- onClick={() => onPick(asset)}
1025
+ onClick={() => onPick(asset, scope)}
863
1026
  className={cn(
864
1027
  'group flex flex-col overflow-hidden rounded-lg border bg-card text-left shadow-sm transition-all',
865
1028
  'hover:-translate-y-0.5 hover:shadow-md focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none',
@@ -953,8 +1116,23 @@ function CommentsSection({
953
1116
  }) {
954
1117
  const [draft, setDraft] = useState('');
955
1118
  const [submitting, setSubmitting] = useState(false);
1119
+ const wrapRef = useRef<HTMLDivElement>(null);
956
1120
  const t = useLocale();
957
1121
 
1122
+ useEffect(() => {
1123
+ const onKey = (e: KeyboardEvent) => {
1124
+ if (e.key !== '/') return;
1125
+ if (!(e.metaKey || e.ctrlKey)) return;
1126
+ if (e.altKey || e.shiftKey) return;
1127
+ const ta = wrapRef.current?.querySelector('textarea');
1128
+ if (!ta) return;
1129
+ e.preventDefault();
1130
+ ta.focus({ preventScroll: true });
1131
+ };
1132
+ window.addEventListener('keydown', onKey);
1133
+ return () => window.removeEventListener('keydown', onKey);
1134
+ }, []);
1135
+
958
1136
  const submit = async () => {
959
1137
  const trimmed = draft.trim();
960
1138
  if (!trimmed) return;
@@ -970,7 +1148,7 @@ function CommentsSection({
970
1148
  return (
971
1149
  <Section title={t.inspector.leaveComment}>
972
1150
  <div className="flex flex-col gap-2">
973
- <div className="comment-cue rounded-[6px]">
1151
+ <div ref={wrapRef} className="comment-cue rounded-[6px]">
974
1152
  <Textarea
975
1153
  value={draft}
976
1154
  onChange={(e) => setDraft(e.target.value)}
@@ -999,7 +1177,7 @@ function CommentsSection({
999
1177
 
1000
1178
  function readSnapshot(el: HTMLElement): ElementSnapshot {
1001
1179
  const cs = getComputedStyle(el);
1002
- const text = isSimpleTextElement(el) ? (el.textContent ?? '') : null;
1180
+ const text = isSimpleTextElement(el) ? readEditableText(el) : null;
1003
1181
  const imageSrc =
1004
1182
  el.tagName === 'IMG'
1005
1183
  ? (el as HTMLImageElement).currentSrc || (el as HTMLImageElement).src || null
@@ -1031,8 +1209,72 @@ function readSnapshot(el: HTMLElement): ElementSnapshot {
1031
1209
 
1032
1210
  function isSimpleTextElement(el: HTMLElement): boolean {
1033
1211
  if (el.childNodes.length === 0) return true;
1034
- if (el.childNodes.length === 1 && el.firstChild?.nodeType === Node.TEXT_NODE) return true;
1035
- return false;
1212
+ return hasOnlyInlineTextChildren(el);
1213
+ }
1214
+
1215
+ const INLINE_TEXT_TAGS = new Set([
1216
+ 'B',
1217
+ 'CODE',
1218
+ 'DEL',
1219
+ 'EM',
1220
+ 'I',
1221
+ 'INS',
1222
+ 'MARK',
1223
+ 'S',
1224
+ 'SMALL',
1225
+ 'SPAN',
1226
+ 'STRONG',
1227
+ 'SUB',
1228
+ 'SUP',
1229
+ 'U',
1230
+ ]);
1231
+
1232
+ function hasOnlyInlineTextChildren(el: HTMLElement): boolean {
1233
+ for (const child of Array.from(el.childNodes)) {
1234
+ if (child.nodeType === Node.TEXT_NODE) {
1235
+ continue;
1236
+ } else if (child instanceof HTMLElement) {
1237
+ if (child.tagName === 'BR') continue;
1238
+ if (INLINE_TEXT_TAGS.has(child.tagName) && hasOnlyInlineTextChildren(child)) continue;
1239
+ }
1240
+ return false;
1241
+ }
1242
+ return true;
1243
+ }
1244
+
1245
+ function readEditableText(el: HTMLElement): string {
1246
+ const parts: string[] = [];
1247
+ for (const child of Array.from(el.childNodes)) {
1248
+ if (child.nodeType === Node.TEXT_NODE) {
1249
+ parts.push(renderedTextNodeValue(child as Text));
1250
+ } else if (child instanceof HTMLBRElement) {
1251
+ parts.push('\n');
1252
+ } else if (child instanceof HTMLElement) {
1253
+ parts.push(readEditableText(child));
1254
+ }
1255
+ }
1256
+ return normalizeRenderedText(parts);
1257
+ }
1258
+
1259
+ function normalizeRenderedText(parts: string[]): string {
1260
+ return parts
1261
+ .map((part, index) => {
1262
+ if (part === '\n') return part;
1263
+ let next = part;
1264
+ if (parts[index - 1] === '\n') next = next.replace(/^\s+/, '');
1265
+ if (parts[index + 1] === '\n') next = next.replace(/\s+$/, '');
1266
+ return next;
1267
+ })
1268
+ .join('');
1269
+ }
1270
+
1271
+ function renderedTextNodeValue(node: Text): string {
1272
+ const value = node.textContent ?? '';
1273
+ const whiteSpace = node.parentElement ? getComputedStyle(node.parentElement).whiteSpace : '';
1274
+ if (whiteSpace === 'pre' || whiteSpace === 'pre-wrap' || whiteSpace === 'break-spaces') {
1275
+ return value;
1276
+ }
1277
+ return value.replace(/\s+/g, ' ');
1036
1278
  }
1037
1279
 
1038
1280
  function rgbToHex(value: string): string | null {