@thangph2146/nextjs-editor 1.0.4 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,7 @@ Trong `app/layout.tsx` hoặc `_app.tsx`:
26
26
  import "@thangph2146/nextjs-editor/styles.css"
27
27
  ```
28
28
 
29
- **Lưu ý:** Chỉ cần import file trên. Bạn **không** cần thêm `@source` hay cấu hình nào trong `globals.css`. Style editor được đóng gói sẵn scope trong `#editor-x` nên không bị style dự án ghi đè.
29
+ **Lưu ý:** Chỉ cần import file trên. Bạn **không** cần thêm `@source` hay cấu hình nào trong `globals.css`. Style editor được đóng gói sẵn, scope trong `#editor-x` nằm trong layer `nextjs-editor` — nếu app dùng CSS layer cho SCSS/global (xem bên dưới), style editor sẽ không bị ghi đè.
30
30
 
31
31
  ### 2. Dùng component Editor (Client Component)
32
32
 
@@ -64,6 +64,54 @@ export function MyEditor() {
64
64
  - **Ảnh**: Mặc định dialog chèn ảnh có tab **URL** và **File**. Để tab **Thư viện** hiển thị ảnh từ API của project, xem [Cấu hình API thư viện ảnh](#cấu-hình-api-thư-viện-ảnh) bên dưới.
65
65
  - **Next.js**: Package dùng `next/image` và `next/dynamic` khi chạy trong Next.js; cần cài `next` trong project.
66
66
 
67
+ ## Tránh SCSS/CSS của app ghi đè editor
68
+
69
+ Style editor được bọc trong **`@layer nextjs-editor`**. Trong CSS, rule **không nằm trong layer** (unlayered) có độ ưu tiên cao hơn rule trong layer, nên nếu SCSS/global của app là unlayered thì có thể ghi đè list, spacing, typography… bên trong `#editor-x`.
70
+
71
+ **Cách làm:** Đưa toàn bộ SCSS/global của app vào một layer (ví dụ `app`) và khai báo thứ tự layer sao cho `nextjs-editor` đứng **sau** — khi đó style editor sẽ thắng khi specificity tương đương.
72
+
73
+ ### Bước 1: Khai báo thứ tự layer trong app
74
+
75
+ Trong `globals.css` (hoặc file CSS chính), **dòng đầu tiên** khai báo thứ tự layer, thêm `app` và `nextjs-editor` (layer xuất hiện sau có ưu tiên cao hơn):
76
+
77
+ ```css
78
+ @layer legacy, base, components, utilities, app, nextjs-editor;
79
+ ```
80
+
81
+ ### Bước 2: Đưa SCSS/global vào layer `app`
82
+
83
+ Mọi style global/SCSS không nằm trong Tailwind cần nằm trong `@layer app`. **Lưu ý:** Trong Sass, `@use` không được phép bên trong `@layer`, nên không bọc trực tiếp nội dung có `@use` trong một block `@layer app { }`.
84
+
85
+ **Cách khuyến nghị — File wrapper dùng `@import`:**
86
+
87
+ Tạo file SCSS mới (ví dụ `src/styles/app-layer.scss`) chỉ dùng `@import` (Sass cho phép `@import` trong `@layer`):
88
+
89
+ ```scss
90
+ @layer app {
91
+ @import "main";
92
+ }
93
+ ```
94
+
95
+ Trong `layout.tsx` (hoặc nơi đang import file SCSS chính), đổi sang import file wrapper:
96
+
97
+ ```tsx
98
+ import "../styles/app-layer.scss";
99
+ ```
100
+
101
+ **Cách khác — File CSS import SCSS (nếu bundler hỗ trợ):**
102
+
103
+ Tạo file `src/app/globals-app.css`:
104
+
105
+ ```css
106
+ @layer app {
107
+ @import "../styles/main.scss";
108
+ }
109
+ ```
110
+
111
+ Rồi trong `layout.tsx` import sau `globals.css`: `import "./globals-app.css"`. (Một số bundler có thể không compile `.scss` khi import từ `.css`.)
112
+
113
+ Sau khi cấu hình, style trong layer `nextjs-editor` (đã load khi import `@thangph2146/nextjs-editor/styles.css`) sẽ có ưu tiên cao hơn layer `app`, nên cấu trúc Tailwind và theme của editor trong `#editor-x` không còn bị SCSS của app ghi đè.
114
+
67
115
  ## Cấu hình API thư viện ảnh
68
116
 
69
117
  Tab **Thư viện** trong dialog chèn ảnh cần dữ liệu từ API/uploads của project. Bạn cấu hình bằng cách **alias** module uploads-hooks của package sang file hooks trong project.
@@ -241,6 +289,12 @@ pnpm build
241
289
  npm publish --access public
242
290
  ```
243
291
 
292
+ **Nếu bật 2FA (one-time password):** thêm mã OTP từ app xác thực (đổi mỗi ~30s):
293
+
294
+ ```bash
295
+ npm publish --access public --otp=XXXXXX
296
+ ```
297
+
244
298
  Nếu đã publish public trước đó, chỉ cần:
245
299
 
246
300
  ```bash
@@ -591,6 +591,9 @@
591
591
  .h-\[var\(--radix-select-trigger-height\)\] {
592
592
  height: var(--radix-select-trigger-height);
593
593
  }
594
+ .h-auto {
595
+ height: auto;
596
+ }
594
597
  .h-full {
595
598
  height: 100%;
596
599
  }
@@ -648,9 +651,6 @@
648
651
  .min-h-\[54px\] {
649
652
  min-height: 54px;
650
653
  }
651
- .min-h-\[70vh\] {
652
- min-height: 70vh;
653
- }
654
654
  .min-h-\[300px\] {
655
655
  min-height: 300px;
656
656
  }
@@ -1135,6 +1135,10 @@
1135
1135
  border-start-end-radius: 0;
1136
1136
  border-end-end-radius: 0;
1137
1137
  }
1138
+ .rounded-t-xl {
1139
+ border-top-left-radius: var(--radius-xl);
1140
+ border-top-right-radius: var(--radius-xl);
1141
+ }
1138
1142
  .rounded-b-xl {
1139
1143
  border-bottom-right-radius: var(--radius-xl);
1140
1144
  border-bottom-left-radius: var(--radius-xl);
@@ -1262,10 +1266,10 @@
1262
1266
  background-color: color-mix(in oklab, var(--background) 95%, transparent);
1263
1267
  }
1264
1268
  }
1265
- .bg-black\/20 {
1266
- background-color: color-mix(in srgb, #000 20%, transparent);
1269
+ .bg-black\/10 {
1270
+ background-color: color-mix(in srgb, #000 10%, transparent);
1267
1271
  @supports (color: color-mix(in lab, red, red)) {
1268
- background-color: color-mix(in oklab, var(--color-black) 20%, transparent);
1272
+ background-color: color-mix(in oklab, var(--color-black) 10%, transparent);
1269
1273
  }
1270
1274
  }
1271
1275
  .bg-black\/60 {
@@ -1328,12 +1332,6 @@
1328
1332
  background-color: color-mix(in oklab, var(--muted) 20%, transparent);
1329
1333
  }
1330
1334
  }
1331
- .bg-muted\/30 {
1332
- background-color: var(--muted);
1333
- @supports (color: color-mix(in lab, red, red)) {
1334
- background-color: color-mix(in oklab, var(--muted) 30%, transparent);
1335
- }
1336
- }
1337
1335
  .bg-muted\/40 {
1338
1336
  background-color: var(--muted);
1339
1337
  @supports (color: color-mix(in lab, red, red)) {
@@ -1527,12 +1525,12 @@
1527
1525
  .pt-0 {
1528
1526
  padding-top: calc(var(--spacing) * 0);
1529
1527
  }
1528
+ .pt-3 {
1529
+ padding-top: calc(var(--spacing) * 3);
1530
+ }
1530
1531
  .pt-4 {
1531
1532
  padding-top: calc(var(--spacing) * 4);
1532
1533
  }
1533
- .pt-5 {
1534
- padding-top: calc(var(--spacing) * 5);
1535
- }
1536
1534
  .pt-6 {
1537
1535
  padding-top: calc(var(--spacing) * 6);
1538
1536
  }
@@ -1542,6 +1540,9 @@
1542
1540
  .pr-10 {
1543
1541
  padding-right: calc(var(--spacing) * 10);
1544
1542
  }
1543
+ .pr-12 {
1544
+ padding-right: calc(var(--spacing) * 12);
1545
+ }
1545
1546
  .pb-2 {
1546
1547
  padding-bottom: calc(var(--spacing) * 2);
1547
1548
  }
@@ -1811,6 +1812,10 @@
1811
1812
  --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1812
1813
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1813
1814
  }
1815
+ .shadow-none {
1816
+ --tw-shadow: 0 0 #0000;
1817
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1818
+ }
1814
1819
  .shadow-sm {
1815
1820
  --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1816
1821
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -2887,6 +2892,11 @@
2887
2892
  padding-block: calc(var(--spacing) * 6);
2888
2893
  }
2889
2894
  }
2895
+ .sm\:text-center {
2896
+ @media (width >= 40rem) {
2897
+ text-align: center;
2898
+ }
2899
+ }
2890
2900
  .sm\:text-left {
2891
2901
  @media (width >= 40rem) {
2892
2902
  text-align: left;
@@ -591,6 +591,9 @@
591
591
  #editor-x .h-\[var\(--radix-select-trigger-height\)\] {
592
592
  height: var(--radix-select-trigger-height);
593
593
  }
594
+ #editor-x .h-auto {
595
+ height: auto;
596
+ }
594
597
  #editor-x .h-full {
595
598
  height: 100%;
596
599
  }
@@ -648,9 +651,6 @@
648
651
  #editor-x .min-h-\[54px\] {
649
652
  min-height: 54px;
650
653
  }
651
- #editor-x .min-h-\[70vh\] {
652
- min-height: 70vh;
653
- }
654
654
  #editor-x .min-h-\[300px\] {
655
655
  min-height: 300px;
656
656
  }
@@ -1135,6 +1135,10 @@
1135
1135
  border-start-end-radius: 0;
1136
1136
  border-end-end-radius: 0;
1137
1137
  }
1138
+ #editor-x .rounded-t-xl {
1139
+ border-top-left-radius: var(--radius-xl);
1140
+ border-top-right-radius: var(--radius-xl);
1141
+ }
1138
1142
  #editor-x .rounded-b-xl {
1139
1143
  border-bottom-right-radius: var(--radius-xl);
1140
1144
  border-bottom-left-radius: var(--radius-xl);
@@ -1262,10 +1266,10 @@
1262
1266
  background-color: color-mix(in oklab, var(--background) 95%, transparent);
1263
1267
  }
1264
1268
  }
1265
- #editor-x .bg-black\/20 {
1266
- background-color: color-mix(in srgb, #000 20%, transparent);
1269
+ #editor-x .bg-black\/10 {
1270
+ background-color: color-mix(in srgb, #000 10%, transparent);
1267
1271
  @supports (color: color-mix(in lab, red, red)) {
1268
- background-color: color-mix(in oklab, var(--color-black) 20%, transparent);
1272
+ background-color: color-mix(in oklab, var(--color-black) 10%, transparent);
1269
1273
  }
1270
1274
  }
1271
1275
  #editor-x .bg-black\/60 {
@@ -1328,12 +1332,6 @@
1328
1332
  background-color: color-mix(in oklab, var(--muted) 20%, transparent);
1329
1333
  }
1330
1334
  }
1331
- #editor-x .bg-muted\/30 {
1332
- background-color: var(--muted);
1333
- @supports (color: color-mix(in lab, red, red)) {
1334
- background-color: color-mix(in oklab, var(--muted) 30%, transparent);
1335
- }
1336
- }
1337
1335
  #editor-x .bg-muted\/40 {
1338
1336
  background-color: var(--muted);
1339
1337
  @supports (color: color-mix(in lab, red, red)) {
@@ -1527,12 +1525,12 @@
1527
1525
  #editor-x .pt-0 {
1528
1526
  padding-top: calc(var(--spacing) * 0);
1529
1527
  }
1528
+ #editor-x .pt-3 {
1529
+ padding-top: calc(var(--spacing) * 3);
1530
+ }
1530
1531
  #editor-x .pt-4 {
1531
1532
  padding-top: calc(var(--spacing) * 4);
1532
1533
  }
1533
- #editor-x .pt-5 {
1534
- padding-top: calc(var(--spacing) * 5);
1535
- }
1536
1534
  #editor-x .pt-6 {
1537
1535
  padding-top: calc(var(--spacing) * 6);
1538
1536
  }
@@ -1542,6 +1540,9 @@
1542
1540
  #editor-x .pr-10 {
1543
1541
  padding-right: calc(var(--spacing) * 10);
1544
1542
  }
1543
+ #editor-x .pr-12 {
1544
+ padding-right: calc(var(--spacing) * 12);
1545
+ }
1545
1546
  #editor-x .pb-2 {
1546
1547
  padding-bottom: calc(var(--spacing) * 2);
1547
1548
  }
@@ -1811,6 +1812,10 @@
1811
1812
  --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1812
1813
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1813
1814
  }
1815
+ #editor-x .shadow-none {
1816
+ --tw-shadow: 0 0 #0000;
1817
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1818
+ }
1814
1819
  #editor-x .shadow-sm {
1815
1820
  --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1816
1821
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -2887,6 +2892,11 @@
2887
2892
  padding-block: calc(var(--spacing) * 6);
2888
2893
  }
2889
2894
  }
2895
+ #editor-x .sm\:text-center {
2896
+ @media (width >= 40rem) {
2897
+ text-align: center;
2898
+ }
2899
+ }
2890
2900
  #editor-x .sm\:text-left {
2891
2901
  @media (width >= 40rem) {
2892
2902
  text-align: left;
package/dist/index.cjs CHANGED
@@ -2123,12 +2123,17 @@ function ContentEditable({
2123
2123
  placeholder: placeholder2,
2124
2124
  className,
2125
2125
  placeholderClassName,
2126
- placeholderDefaults = true
2126
+ placeholderDefaults = true,
2127
+ readOnly = false
2127
2128
  }) {
2128
2129
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2129
2130
  import_LexicalContentEditable.ContentEditable,
2130
2131
  {
2131
- className: cn("ContentEditable__root relative block min-h-72 px-8 py-4 focus:outline-none", className),
2132
+ className: cn(
2133
+ "ContentEditable__root relative block px-8 py-4 focus:outline-none",
2134
+ readOnly ? "min-h-0" : "min-h-72",
2135
+ className
2136
+ ),
2132
2137
  "aria-placeholder": placeholder2,
2133
2138
  "aria-label": placeholder2 || "Editor n\u1ED9i dung",
2134
2139
  placeholder: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
@@ -2617,20 +2622,39 @@ function ImageComponent({
2617
2622
  e.stopPropagation();
2618
2623
  setIsViewOriginalOpen(true);
2619
2624
  }, [isEditable]);
2620
- const imageWrapper = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { draggable, children: isLoadError ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(BrokenImage, {}) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2621
- LazyImage,
2625
+ const imageWrapper = /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2626
+ "div",
2622
2627
  {
2623
- className: imageClassName,
2624
- src,
2625
- altText,
2626
- imageRef,
2627
- width: responsiveDimensions.width,
2628
- height: responsiveDimensions.height,
2629
- maxWidth,
2630
- onError: () => setIsLoadError(true),
2631
- fetchPriority: isPriority ? "high" : "auto"
2628
+ draggable,
2629
+ ...!isEditable && {
2630
+ role: "button",
2631
+ tabIndex: 0,
2632
+ onClick: handleViewOriginal,
2633
+ onKeyDown: (e) => {
2634
+ if (e.key === "Enter" || e.key === " ") {
2635
+ e.preventDefault();
2636
+ handleViewOriginal(e);
2637
+ }
2638
+ },
2639
+ style: { cursor: "pointer", pointerEvents: "auto" },
2640
+ className: "block w-full"
2641
+ },
2642
+ children: isLoadError ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(BrokenImage, {}) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2643
+ LazyImage,
2644
+ {
2645
+ className: imageClassName,
2646
+ src,
2647
+ altText,
2648
+ imageRef,
2649
+ width: responsiveDimensions.width,
2650
+ height: responsiveDimensions.height,
2651
+ maxWidth,
2652
+ onError: () => setIsLoadError(true),
2653
+ fetchPriority: isPriority ? "high" : "auto"
2654
+ }
2655
+ )
2632
2656
  }
2633
- ) });
2657
+ );
2634
2658
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2635
2659
  import_react11.Suspense,
2636
2660
  {
@@ -2647,7 +2671,8 @@ function ImageComponent({
2647
2671
  {
2648
2672
  type: "button",
2649
2673
  onClick: handleViewOriginal,
2650
- className: "cursor-pointer border-0 bg-transparent p-0 text-left outline-none [&>div]:outline-none",
2674
+ className: "block w-full cursor-pointer border-0 bg-transparent p-0 text-left outline-none [&>div]:outline-none",
2675
+ style: { pointerEvents: "auto" },
2651
2676
  "aria-label": "Xem h\xECnh g\u1ED1c",
2652
2677
  children: imageWrapper
2653
2678
  }
@@ -2698,29 +2723,29 @@ function ImageComponent({
2698
2723
  className: "max-w-[96vw] max-h-[96vh] w-[96vw] h-[96vh] overflow-hidden flex flex-col p-0 gap-0 rounded-xl border shadow-2xl",
2699
2724
  "aria-describedby": "view-original-image-desc",
2700
2725
  children: [
2701
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DialogHeader, { className: "shrink-0 px-6 pt-5 pb-2 border-b bg-muted/30", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DialogTitle, { className: "text-base font-semibold", children: "Xem h\xECnh g\u1ED1c" }) }),
2726
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DialogHeader, { className: "shrink-0 px-4 pt-3 pb-3 pr-12 border-b border-border/50 bg-muted/20 rounded-t-xl text-center sm:text-center", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DialogTitle, { className: "text-sm font-semibold", children: "Xem h\xECnh g\u1ED1c" }) }),
2702
2727
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2703
2728
  "div",
2704
2729
  {
2705
2730
  id: "view-original-image-desc",
2706
- className: "flex-1 min-h-[70vh] overflow-auto flex items-center justify-center p-4 bg-black/20",
2707
- children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2731
+ className: "flex-1 min-h-0 w-full max-w-full overflow-x-hidden overflow-y-auto flex items-center justify-center p-4 bg-black/10",
2732
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "max-w-full max-h-full min-w-0 min-h-0 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2708
2733
  "img",
2709
2734
  {
2710
2735
  src,
2711
2736
  alt: altText || "H\xECnh g\u1ED1c",
2712
- className: "flex-1 min-w-0 min-h-0 w-full h-full max-w-full max-h-full object-contain rounded-lg"
2737
+ className: "min-w-0 min-h-0 max-w-full max-h-full w-auto h-auto object-contain rounded-lg select-none"
2713
2738
  }
2714
- )
2739
+ ) })
2715
2740
  }
2716
2741
  ),
2717
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DialogFooter, { className: "shrink-0 flex-row justify-center gap-2 px-6 py-4 border-t bg-muted/20 rounded-b-xl", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2742
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DialogFooter, { className: "shrink-0 flex-row justify-center gap-2 px-4 py-3 border-t border-border/50 bg-muted/20 rounded-b-xl", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2718
2743
  Button,
2719
2744
  {
2720
2745
  variant: "outline",
2721
- size: "default",
2746
+ size: "sm",
2722
2747
  asChild: true,
2723
- className: "gap-2 text-base",
2748
+ className: "gap-2",
2724
2749
  children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
2725
2750
  "a",
2726
2751
  {
@@ -33210,7 +33235,8 @@ function Plugins({
33210
33235
  ContentEditable,
33211
33236
  {
33212
33237
  placeholder: readOnly ? "" : placeholder,
33213
- className: `ContentEditable__root relative block min-h-72 px-8 py-4 focus:outline-none ${readOnly ? "cursor-default select-text border border-border rounded-md" : ""}`
33238
+ readOnly,
33239
+ className: readOnly ? "cursor-default select-text border-0 bg-transparent shadow-none" : ""
33214
33240
  }
33215
33241
  ) }) }),
33216
33242
  ErrorBoundary: import_LexicalErrorBoundary2.LexicalErrorBoundary,
@@ -33386,9 +33412,11 @@ function Editor({
33386
33412
  {
33387
33413
  ref: editorRef,
33388
33414
  className: cn(
33389
- "bg-background rounded-lg shadow w-full"
33415
+ "bg-background rounded-lg w-full",
33416
+ readOnly ? "shadow-none" : "shadow"
33390
33417
  ),
33391
33418
  id: "editor-x",
33419
+ "data-readonly": readOnly ? "true" : void 0,
33392
33420
  children: /* @__PURE__ */ (0, import_jsx_runtime96.jsx)(EditorContainerProvider, { value: { maxWidth: editorMaxWidth }, children: /* @__PURE__ */ (0, import_jsx_runtime96.jsx)(
33393
33421
  import_LexicalComposer.LexicalComposer,
33394
33422
  {