@skalfa/skalfa-component 1.0.28 → 1.0.29

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.
@@ -308,7 +308,102 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
308
308
  saveSelection();
309
309
  handleEditorChange();
310
310
  }, [restoreSelection, saveSelection, handleEditorChange]);
311
+ const updateActiveStates = (0, react_1.useCallback)(() => {
312
+ const sel = window.getSelection();
313
+ let inH2 = false;
314
+ if (sel && sel.rangeCount > 0) {
315
+ let node = sel.anchorNode;
316
+ while (node && node !== editorRef.current) {
317
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "H2") {
318
+ inH2 = true;
319
+ break;
320
+ }
321
+ node = node.parentNode;
322
+ }
323
+ }
324
+ setActiveStates({
325
+ header: inH2,
326
+ bold: isCommandActive("bold"),
327
+ italic: isCommandActive("italic"),
328
+ underline: isCommandActive("underline"),
329
+ strikeThrough: isCommandActive("strikeThrough"),
330
+ justifyLeft: isCommandActive("justifyLeft"),
331
+ justifyCenter: isCommandActive("justifyCenter"),
332
+ justifyRight: isCommandActive("justifyRight"),
333
+ justifyFull: isCommandActive("justifyFull"),
334
+ insertUnorderedList: isCommandActive("insertUnorderedList"),
335
+ insertOrderedList: isCommandActive("insertOrderedList"),
336
+ });
337
+ if (sel && sel.rangeCount > 0) {
338
+ let node = sel.anchorNode;
339
+ let detectedColor = "normal";
340
+ let detectedSize = null;
341
+ while (node && node !== editorRef.current) {
342
+ if (node.nodeType === Node.ELEMENT_NODE) {
343
+ const el = node;
344
+ if (el.dataset.color && detectedColor === "normal") {
345
+ detectedColor = el.dataset.color;
346
+ }
347
+ if (el.dataset.size && detectedSize === null) {
348
+ const parsed = parseInt(el.dataset.size);
349
+ if (!isNaN(parsed))
350
+ detectedSize = parsed;
351
+ }
352
+ }
353
+ node = node.parentNode;
354
+ }
355
+ setActiveColor(detectedColor);
356
+ if (detectedSize !== null) {
357
+ setTextSize(detectedSize);
358
+ }
359
+ }
360
+ }, [isCommandActive]);
361
+ (0, react_1.useEffect)(() => {
362
+ _utils_1.shortcut.register("ctrl+b", () => handleBold(), "Bold text");
363
+ _utils_1.shortcut.register("ctrl+i", () => handleItalic(), "Italic text");
364
+ _utils_1.shortcut.register("ctrl+u", () => handleUnderline(), "Underline text");
365
+ _utils_1.shortcut.register("ctrl+shift+x", () => handleStrikethrough(), "Strikethrough text");
366
+ _utils_1.shortcut.register("ctrl+k", () => handleLink(), "Insert link");
367
+ return () => {
368
+ _utils_1.shortcut.unregister("ctrl+b");
369
+ _utils_1.shortcut.unregister("ctrl+i");
370
+ _utils_1.shortcut.unregister("ctrl+u");
371
+ _utils_1.shortcut.unregister("ctrl+shift+x");
372
+ _utils_1.shortcut.unregister("ctrl+k");
373
+ };
374
+ }, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink]);
311
375
  const handleKeyDown = (0, react_1.useCallback)((e) => {
376
+ const isMod = e.ctrlKey || e.metaKey;
377
+ const key = e.key.toLowerCase();
378
+ if (isMod && !e.shiftKey && !e.altKey && key === "b") {
379
+ e.preventDefault();
380
+ handleBold();
381
+ updateActiveStates();
382
+ return;
383
+ }
384
+ if (isMod && !e.shiftKey && !e.altKey && key === "i") {
385
+ e.preventDefault();
386
+ handleItalic();
387
+ updateActiveStates();
388
+ return;
389
+ }
390
+ if (isMod && !e.shiftKey && !e.altKey && key === "u") {
391
+ e.preventDefault();
392
+ handleUnderline();
393
+ updateActiveStates();
394
+ return;
395
+ }
396
+ if (isMod && e.shiftKey && !e.altKey && key === "x") {
397
+ e.preventDefault();
398
+ handleStrikethrough();
399
+ updateActiveStates();
400
+ return;
401
+ }
402
+ if (isMod && !e.shiftKey && !e.altKey && key === "k") {
403
+ e.preventDefault();
404
+ handleLink();
405
+ return;
406
+ }
312
407
  if (e.key === "Backspace") {
313
408
  const sel = window.getSelection();
314
409
  if (sel && sel.rangeCount > 0 && sel.isCollapsed) {
@@ -362,75 +457,41 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
362
457
  }
363
458
  }
364
459
  }
365
- }, [handleEditorChange]);
366
- const updateActiveStates = (0, react_1.useCallback)(() => {
367
- const sel = window.getSelection();
368
- let inH2 = false;
369
- if (sel && sel.rangeCount > 0) {
370
- let node = sel.anchorNode;
371
- while (node && node !== editorRef.current) {
372
- if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "H2") {
373
- inH2 = true;
374
- break;
375
- }
376
- node = node.parentNode;
377
- }
378
- }
379
- setActiveStates({
380
- header: inH2,
381
- bold: isCommandActive("bold"),
382
- italic: isCommandActive("italic"),
383
- underline: isCommandActive("underline"),
384
- strikeThrough: isCommandActive("strikeThrough"),
385
- justifyLeft: isCommandActive("justifyLeft"),
386
- justifyCenter: isCommandActive("justifyCenter"),
387
- justifyRight: isCommandActive("justifyRight"),
388
- justifyFull: isCommandActive("justifyFull"),
389
- insertUnorderedList: isCommandActive("insertUnorderedList"),
390
- insertOrderedList: isCommandActive("insertOrderedList"),
391
- });
392
- if (sel && sel.rangeCount > 0) {
393
- let node = sel.anchorNode;
394
- let detectedColor = "normal";
395
- let detectedSize = null;
396
- while (node && node !== editorRef.current) {
397
- if (node.nodeType === Node.ELEMENT_NODE) {
398
- const el = node;
399
- if (el.dataset.color && detectedColor === "normal") {
400
- detectedColor = el.dataset.color;
401
- }
402
- if (el.dataset.size && detectedSize === null) {
403
- const parsed = parseInt(el.dataset.size);
404
- if (!isNaN(parsed))
405
- detectedSize = parsed;
406
- }
407
- }
408
- node = node.parentNode;
409
- }
410
- setActiveColor(detectedColor);
411
- if (detectedSize !== null) {
412
- setTextSize(detectedSize);
413
- }
414
- }
415
- }, [isCommandActive]);
460
+ }, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink, handleEditorChange, updateActiveStates]);
416
461
  const renderControlItem = (0, react_1.useCallback)((item, index) => {
417
462
  if (typeof item !== "string") {
418
463
  return (0, jsx_runtime_1.jsx)("div", { children: item }, index);
419
464
  }
420
465
  switch (item) {
421
- case "HEADER":
422
- return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.header ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-h", tips: "Header", className: (0, _utils_1.cn)("toolbar-btn", activeStates.header && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleHeader(); } }, index));
466
+ // case "HEADER":
467
+ // return (
468
+ // <ButtonComponent
469
+ // key={index}
470
+ // variant={activeStates.header ? "light" : "simple"}
471
+ // paint="primary"
472
+ // size="sm"
473
+ // icon="solid/text-h"
474
+ // tips="Header"
475
+ // className={cn("toolbar-btn", activeStates.header && "toolbar-btn-active")}
476
+ // onClick={(e: any) => { e?.preventDefault?.(); handleHeader(); }}
477
+ // />
478
+ // );
423
479
  case "TEXT_SIZE":
424
480
  case "FONT_SIZE":
425
481
  return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-0.5", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: "simple", paint: "primary", size: "sm", icon: "solid/minus", tips: "Decrease Font Size", className: "toolbar-btn", onClick: (e) => { e?.preventDefault?.(); handleFontSizeChange(textSize - 1); } }), (0, jsx_runtime_1.jsx)("input", { type: "number", value: textSize, onChange: (e) => handleFontSizeChange(parseInt(e.target.value) || 14), onFocus: () => saveSelection(), className: "w-10 h-7 text-xs text-center border border-stroke rounded focus:outline-none focus:border-primary bg-background text-foreground select-none" }), (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: "simple", paint: "primary", size: "sm", icon: "solid/plus", tips: "Increase Font Size", className: "toolbar-btn", onClick: (e) => { e?.preventDefault?.(); handleFontSizeChange(textSize + 1); } })] }, index));
482
+ case "COLOR":
483
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeColor !== "normal" ? "light" : "simple", paint: "primary", size: "sm", label: (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("div", { className: "skcontent-color-dot", style: { backgroundColor: ContentWrapper_component_1.COLOR_MAP[activeColor]?.css || ContentWrapper_component_1.COLOR_MAP.normal.css } }) }), tips: `Text Color (${ContentWrapper_component_1.COLOR_MAP[activeColor]?.label || "Normal"})`, className: (0, _utils_1.cn)("toolbar-btn", (activeColor !== "normal" || showColorPicker) && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); setShowColorPicker(!showColorPicker); } }), showColorPicker && ((0, jsx_runtime_1.jsx)("div", { className: "skcontent-dropdown skcontent-color-picker", children: Object.entries(ContentWrapper_component_1.COLOR_MAP).map(([key, info]) => ((0, jsx_runtime_1.jsx)("button", { type: "button", title: info.label, className: (0, _utils_1.cn)("skcontent-color-dot", activeColor === key && "scale-125 border-2 border-primary"), style: { backgroundColor: info.css }, onMouseDown: (e) => {
484
+ e.preventDefault();
485
+ handleColor(key);
486
+ } }, key))) }))] }, index));
426
487
  case "BOLD":
427
- return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.bold ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-b", tips: "Bold", className: (0, _utils_1.cn)("toolbar-btn", activeStates.bold && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleBold(); } }, index));
488
+ return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.bold ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-b", tips: "Bold (Ctrl+B)", className: (0, _utils_1.cn)("toolbar-btn", activeStates.bold && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleBold(); } }, index));
428
489
  case "ITALIC":
429
- return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.italic ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-italic", tips: "Italic", className: (0, _utils_1.cn)("toolbar-btn", activeStates.italic && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleItalic(); } }, index));
490
+ return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.italic ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-italic", tips: "Italic (Ctrl+I)", className: (0, _utils_1.cn)("toolbar-btn", activeStates.italic && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleItalic(); } }, index));
430
491
  case "UNDERLINE":
431
- return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.underline ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-underline", tips: "Underline", className: (0, _utils_1.cn)("toolbar-btn", activeStates.underline && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleUnderline(); } }, index));
492
+ return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.underline ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-underline", tips: "Underline (Ctrl+U)", className: (0, _utils_1.cn)("toolbar-btn", activeStates.underline && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleUnderline(); } }, index));
432
493
  case "STRIKETHROUGH":
433
- return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.strikeThrough ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-slash", tips: "Strikethrough", className: (0, _utils_1.cn)("toolbar-btn", activeStates.strikeThrough && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleStrikethrough(); } }, index));
494
+ return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.strikeThrough ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-slash", tips: "Strikethrough (Ctrl+Shift+X)", className: (0, _utils_1.cn)("toolbar-btn", activeStates.strikeThrough && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleStrikethrough(); } }, index));
434
495
  case "ALIGN_LEFT":
435
496
  return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.justifyLeft ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-left", tips: "Align Left", className: (0, _utils_1.cn)("toolbar-btn", activeStates.justifyLeft && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleAlign("left"); } }, index));
436
497
  case "ALIGN_CENTER":
@@ -440,7 +501,7 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
440
501
  case "ALIGN_JUSTIFY":
441
502
  return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.justifyFull ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/text-justify", tips: "Align Justify", className: (0, _utils_1.cn)("toolbar-btn", activeStates.justifyFull && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleAlign("justify"); } }, index));
442
503
  case "LINK":
443
- return ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: showLinkInput ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/link", tips: "Link", className: (0, _utils_1.cn)("toolbar-btn", showLinkInput && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleLink(); } }), showLinkInput && ((0, jsx_runtime_1.jsxs)("div", { className: "skcontent-dropdown skcontent-link-input", children: [(0, jsx_runtime_1.jsx)("input", { type: "url", placeholder: "https://...", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), onKeyDown: (e) => {
504
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: showLinkInput ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/link", tips: "Link (Ctrl+K)", className: (0, _utils_1.cn)("toolbar-btn", showLinkInput && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleLink(); } }), showLinkInput && ((0, jsx_runtime_1.jsxs)("div", { className: "skcontent-dropdown skcontent-link-input", children: [(0, jsx_runtime_1.jsx)("input", { type: "url", placeholder: "https://...", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), onKeyDown: (e) => {
444
505
  if (e.key === "Enter") {
445
506
  e.preventDefault();
446
507
  handleLinkSubmit();
@@ -449,11 +510,6 @@ function InputContentComponent({ label, tip, className = "", value, invalid, val
449
510
  setShowLinkInput(false);
450
511
  }
451
512
  }, className: "skcontent-link-url-input", autoFocus: true }), (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: "solid", paint: "primary", size: "xs", icon: "solid/check", onClick: (e) => { e?.preventDefault?.(); handleLinkSubmit(); } })] }))] }, index));
452
- case "COLOR":
453
- return ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeColor !== "normal" ? "light" : "simple", paint: "primary", size: "sm", label: (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("div", { className: "skcontent-color-dot", style: { backgroundColor: ContentWrapper_component_1.COLOR_MAP[activeColor]?.css || ContentWrapper_component_1.COLOR_MAP.normal.css } }) }), tips: `Text Color (${ContentWrapper_component_1.COLOR_MAP[activeColor]?.label || "Normal"})`, className: (0, _utils_1.cn)("toolbar-btn", (activeColor !== "normal" || showColorPicker) && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); setShowColorPicker(!showColorPicker); } }), showColorPicker && ((0, jsx_runtime_1.jsx)("div", { className: "skcontent-dropdown skcontent-color-picker", children: Object.entries(ContentWrapper_component_1.COLOR_MAP).map(([key, info]) => ((0, jsx_runtime_1.jsx)("button", { type: "button", title: info.label, className: (0, _utils_1.cn)("skcontent-color-dot", activeColor === key && "scale-125 border-2 border-primary"), style: { backgroundColor: info.css }, onMouseDown: (e) => {
454
- e.preventDefault();
455
- handleColor(key);
456
- } }, key))) }))] }, index));
457
513
  case "LIST_BULLET":
458
514
  case "BULLET_LIST":
459
515
  return ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { variant: activeStates.insertUnorderedList ? "light" : "simple", paint: "primary", size: "sm", icon: "solid/list-bullet", tips: "Bullet List", className: (0, _utils_1.cn)("toolbar-btn", activeStates.insertUnorderedList && "toolbar-btn-active"), onClick: (e) => { e?.preventDefault?.(); handleBulletList(); } }, index));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-component",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "description": "Reusable UI components for Skalfa App.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { ReactNode, Ref, useCallback, useEffect, useRef, useState } from "react";
4
4
  import { Icon } from "@skalfa/skalfa-icon";
5
- import { cn, pcn, useInputHandler, useInputRandomId, useValidation, ValidationRules } from "@utils";
5
+ import { cn, pcn, shortcut, useInputHandler, useInputRandomId, useValidation, ValidationRules } from "@utils";
6
6
  import { COLOR_MAP, parseContentToHtml, parseHtmlToContent } from "../wrap/ContentWrapper.component";
7
7
  import { ButtonComponent } from "../button/Button.component";
8
8
 
@@ -408,7 +408,114 @@ export function InputContentComponent({
408
408
  handleEditorChange();
409
409
  }, [restoreSelection, saveSelection, handleEditorChange]);
410
410
 
411
+
412
+
413
+ const updateActiveStates = useCallback(() => {
414
+ const sel = window.getSelection();
415
+ let inH2 = false;
416
+ if (sel && sel.rangeCount > 0) {
417
+ let node: Node | null = sel.anchorNode;
418
+ while (node && node !== editorRef.current) {
419
+ if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "H2") {
420
+ inH2 = true;
421
+ break;
422
+ }
423
+ node = node.parentNode;
424
+ }
425
+ }
426
+
427
+ setActiveStates({
428
+ header: inH2,
429
+ bold: isCommandActive("bold"),
430
+ italic: isCommandActive("italic"),
431
+ underline: isCommandActive("underline"),
432
+ strikeThrough: isCommandActive("strikeThrough"),
433
+ justifyLeft: isCommandActive("justifyLeft"),
434
+ justifyCenter: isCommandActive("justifyCenter"),
435
+ justifyRight: isCommandActive("justifyRight"),
436
+ justifyFull: isCommandActive("justifyFull"),
437
+ insertUnorderedList: isCommandActive("insertUnorderedList"),
438
+ insertOrderedList: isCommandActive("insertOrderedList"),
439
+ });
440
+
441
+ if (sel && sel.rangeCount > 0) {
442
+ let node: Node | null = sel.anchorNode;
443
+ let detectedColor = "normal";
444
+ let detectedSize: number | null = null;
445
+ while (node && node !== editorRef.current) {
446
+ if (node.nodeType === Node.ELEMENT_NODE) {
447
+ const el = node as HTMLElement;
448
+ if (el.dataset.color && detectedColor === "normal") {
449
+ detectedColor = el.dataset.color;
450
+ }
451
+ if (el.dataset.size && detectedSize === null) {
452
+ const parsed = parseInt(el.dataset.size);
453
+ if (!isNaN(parsed)) detectedSize = parsed;
454
+ }
455
+ }
456
+ node = node.parentNode;
457
+ }
458
+ setActiveColor(detectedColor);
459
+ if (detectedSize !== null) {
460
+ setTextSize(detectedSize);
461
+ }
462
+ }
463
+ }, [isCommandActive]);
464
+
465
+ useEffect(() => {
466
+ shortcut.register("ctrl+b", () => handleBold(), "Bold text");
467
+ shortcut.register("ctrl+i", () => handleItalic(), "Italic text");
468
+ shortcut.register("ctrl+u", () => handleUnderline(), "Underline text");
469
+ shortcut.register("ctrl+shift+x", () => handleStrikethrough(), "Strikethrough text");
470
+ shortcut.register("ctrl+k", () => handleLink(), "Insert link");
471
+
472
+ return () => {
473
+ shortcut.unregister("ctrl+b");
474
+ shortcut.unregister("ctrl+i");
475
+ shortcut.unregister("ctrl+u");
476
+ shortcut.unregister("ctrl+shift+x");
477
+ shortcut.unregister("ctrl+k");
478
+ };
479
+ }, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink]);
480
+
411
481
  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
482
+ const isMod = e.ctrlKey || e.metaKey;
483
+ const key = e.key.toLowerCase();
484
+
485
+ if (isMod && !e.shiftKey && !e.altKey && key === "b") {
486
+ e.preventDefault();
487
+ handleBold();
488
+ updateActiveStates();
489
+ return;
490
+ }
491
+
492
+ if (isMod && !e.shiftKey && !e.altKey && key === "i") {
493
+ e.preventDefault();
494
+ handleItalic();
495
+ updateActiveStates();
496
+ return;
497
+ }
498
+
499
+ if (isMod && !e.shiftKey && !e.altKey && key === "u") {
500
+ e.preventDefault();
501
+ handleUnderline();
502
+ updateActiveStates();
503
+ return;
504
+ }
505
+
506
+ if (isMod && e.shiftKey && !e.altKey && key === "x") {
507
+ e.preventDefault();
508
+ handleStrikethrough();
509
+ updateActiveStates();
510
+ return;
511
+ }
512
+
513
+ if (isMod && !e.shiftKey && !e.altKey && key === "k") {
514
+ e.preventDefault();
515
+ handleLink();
516
+ return;
517
+ }
518
+
412
519
  if (e.key === "Backspace") {
413
520
  const sel = window.getSelection();
414
521
  if (sel && sel.rangeCount > 0 && sel.isCollapsed) {
@@ -465,59 +572,7 @@ export function InputContentComponent({
465
572
  }
466
573
  }
467
574
  }
468
- }, [handleEditorChange]);
469
-
470
- const updateActiveStates = useCallback(() => {
471
- const sel = window.getSelection();
472
- let inH2 = false;
473
- if (sel && sel.rangeCount > 0) {
474
- let node: Node | null = sel.anchorNode;
475
- while (node && node !== editorRef.current) {
476
- if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "H2") {
477
- inH2 = true;
478
- break;
479
- }
480
- node = node.parentNode;
481
- }
482
- }
483
-
484
- setActiveStates({
485
- header: inH2,
486
- bold: isCommandActive("bold"),
487
- italic: isCommandActive("italic"),
488
- underline: isCommandActive("underline"),
489
- strikeThrough: isCommandActive("strikeThrough"),
490
- justifyLeft: isCommandActive("justifyLeft"),
491
- justifyCenter: isCommandActive("justifyCenter"),
492
- justifyRight: isCommandActive("justifyRight"),
493
- justifyFull: isCommandActive("justifyFull"),
494
- insertUnorderedList: isCommandActive("insertUnorderedList"),
495
- insertOrderedList: isCommandActive("insertOrderedList"),
496
- });
497
-
498
- if (sel && sel.rangeCount > 0) {
499
- let node: Node | null = sel.anchorNode;
500
- let detectedColor = "normal";
501
- let detectedSize: number | null = null;
502
- while (node && node !== editorRef.current) {
503
- if (node.nodeType === Node.ELEMENT_NODE) {
504
- const el = node as HTMLElement;
505
- if (el.dataset.color && detectedColor === "normal") {
506
- detectedColor = el.dataset.color;
507
- }
508
- if (el.dataset.size && detectedSize === null) {
509
- const parsed = parseInt(el.dataset.size);
510
- if (!isNaN(parsed)) detectedSize = parsed;
511
- }
512
- }
513
- node = node.parentNode;
514
- }
515
- setActiveColor(detectedColor);
516
- if (detectedSize !== null) {
517
- setTextSize(detectedSize);
518
- }
519
- }
520
- }, [isCommandActive]);
575
+ }, [handleBold, handleItalic, handleUnderline, handleStrikethrough, handleLink, handleEditorChange, updateActiveStates]);
521
576
 
522
577
  const renderControlItem = useCallback((item: ToolbarControlItem, index: number) => {
523
578
  if (typeof item !== "string") {
@@ -525,19 +580,19 @@ export function InputContentComponent({
525
580
  }
526
581
 
527
582
  switch (item) {
528
- case "HEADER":
529
- return (
530
- <ButtonComponent
531
- key={index}
532
- variant={activeStates.header ? "light" : "simple"}
533
- paint="primary"
534
- size="sm"
535
- icon="solid/text-h"
536
- tips="Header"
537
- className={cn("toolbar-btn", activeStates.header && "toolbar-btn-active")}
538
- onClick={(e: any) => { e?.preventDefault?.(); handleHeader(); }}
539
- />
540
- );
583
+ // case "HEADER":
584
+ // return (
585
+ // <ButtonComponent
586
+ // key={index}
587
+ // variant={activeStates.header ? "light" : "simple"}
588
+ // paint="primary"
589
+ // size="sm"
590
+ // icon="solid/text-h"
591
+ // tips="Header"
592
+ // className={cn("toolbar-btn", activeStates.header && "toolbar-btn-active")}
593
+ // onClick={(e: any) => { e?.preventDefault?.(); handleHeader(); }}
594
+ // />
595
+ // );
541
596
  case "TEXT_SIZE":
542
597
  case "FONT_SIZE":
543
598
  return (
@@ -569,6 +624,41 @@ export function InputContentComponent({
569
624
  />
570
625
  </div>
571
626
  );
627
+ case "COLOR":
628
+ return (
629
+ <div className="relative" key={index}>
630
+ <ButtonComponent
631
+ variant={activeColor !== "normal" ? "light" : "simple"}
632
+ paint="primary"
633
+ size="sm"
634
+ label={<div><div className="skcontent-color-dot" style={{ backgroundColor: COLOR_MAP[activeColor]?.css || COLOR_MAP.normal.css }} /></div>}
635
+ tips={`Text Color (${COLOR_MAP[activeColor]?.label || "Normal"})`}
636
+ className={cn("toolbar-btn", (activeColor !== "normal" || showColorPicker) && "toolbar-btn-active")}
637
+ onClick={(e: any) => { e?.preventDefault?.(); setShowColorPicker(!showColorPicker); }}
638
+ />
639
+
640
+ {showColorPicker && (
641
+ <div className="skcontent-dropdown skcontent-color-picker">
642
+ {Object.entries(COLOR_MAP).map(([key, info]) => (
643
+ <button
644
+ key={key}
645
+ type="button"
646
+ title={info.label}
647
+ className={cn(
648
+ "skcontent-color-dot",
649
+ activeColor === key && "scale-125 border-2 border-primary"
650
+ )}
651
+ style={{ backgroundColor: info.css }}
652
+ onMouseDown={(e) => {
653
+ e.preventDefault();
654
+ handleColor(key);
655
+ }}
656
+ />
657
+ ))}
658
+ </div>
659
+ )}
660
+ </div>
661
+ );
572
662
  case "BOLD":
573
663
  return (
574
664
  <ButtonComponent
@@ -577,7 +667,7 @@ export function InputContentComponent({
577
667
  paint="primary"
578
668
  size="sm"
579
669
  icon="solid/text-b"
580
- tips="Bold"
670
+ tips="Bold (Ctrl+B)"
581
671
  className={cn("toolbar-btn", activeStates.bold && "toolbar-btn-active")}
582
672
  onClick={(e: any) => { e?.preventDefault?.(); handleBold(); }}
583
673
  />
@@ -590,7 +680,7 @@ export function InputContentComponent({
590
680
  paint="primary"
591
681
  size="sm"
592
682
  icon="solid/text-italic"
593
- tips="Italic"
683
+ tips="Italic (Ctrl+I)"
594
684
  className={cn("toolbar-btn", activeStates.italic && "toolbar-btn-active")}
595
685
  onClick={(e: any) => { e?.preventDefault?.(); handleItalic(); }}
596
686
  />
@@ -603,7 +693,7 @@ export function InputContentComponent({
603
693
  paint="primary"
604
694
  size="sm"
605
695
  icon="solid/text-underline"
606
- tips="Underline"
696
+ tips="Underline (Ctrl+U)"
607
697
  className={cn("toolbar-btn", activeStates.underline && "toolbar-btn-active")}
608
698
  onClick={(e: any) => { e?.preventDefault?.(); handleUnderline(); }}
609
699
  />
@@ -616,7 +706,7 @@ export function InputContentComponent({
616
706
  paint="primary"
617
707
  size="sm"
618
708
  icon="solid/text-slash"
619
- tips="Strikethrough"
709
+ tips="Strikethrough (Ctrl+Shift+X)"
620
710
  className={cn("toolbar-btn", activeStates.strikeThrough && "toolbar-btn-active")}
621
711
  onClick={(e: any) => { e?.preventDefault?.(); handleStrikethrough(); }}
622
712
  />
@@ -681,7 +771,7 @@ export function InputContentComponent({
681
771
  paint="primary"
682
772
  size="sm"
683
773
  icon="solid/link"
684
- tips="Link"
774
+ tips="Link (Ctrl+K)"
685
775
  className={cn("toolbar-btn", showLinkInput && "toolbar-btn-active")}
686
776
  onClick={(e: any) => { e?.preventDefault?.(); handleLink(); }}
687
777
  />
@@ -716,41 +806,6 @@ export function InputContentComponent({
716
806
  )}
717
807
  </div>
718
808
  );
719
- case "COLOR":
720
- return (
721
- <div className="relative" key={index}>
722
- <ButtonComponent
723
- variant={activeColor !== "normal" ? "light" : "simple"}
724
- paint="primary"
725
- size="sm"
726
- label={<div><div className="skcontent-color-dot" style={{ backgroundColor: COLOR_MAP[activeColor]?.css || COLOR_MAP.normal.css }} /></div>}
727
- tips={`Text Color (${COLOR_MAP[activeColor]?.label || "Normal"})`}
728
- className={cn("toolbar-btn", (activeColor !== "normal" || showColorPicker) && "toolbar-btn-active")}
729
- onClick={(e: any) => { e?.preventDefault?.(); setShowColorPicker(!showColorPicker); }}
730
- />
731
-
732
- {showColorPicker && (
733
- <div className="skcontent-dropdown skcontent-color-picker">
734
- {Object.entries(COLOR_MAP).map(([key, info]) => (
735
- <button
736
- key={key}
737
- type="button"
738
- title={info.label}
739
- className={cn(
740
- "skcontent-color-dot",
741
- activeColor === key && "scale-125 border-2 border-primary"
742
- )}
743
- style={{ backgroundColor: info.css }}
744
- onMouseDown={(e) => {
745
- e.preventDefault();
746
- handleColor(key);
747
- }}
748
- />
749
- ))}
750
- </div>
751
- )}
752
- </div>
753
- );
754
809
  case "LIST_BULLET":
755
810
  case "BULLET_LIST":
756
811
  return (