@goplusvn/core 0.1.74 → 0.1.75

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.74",
4
+ "version": "0.1.75",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -53,11 +53,11 @@ export function Auth({
53
53
  >
54
54
  <div className="flex flex-col relative bg-background">
55
55
  <div className="absolute top-0 inset-x-0 flex justify-between items-center gap-2 px-4 py-2.5 z-50">
56
- {/* Logo chỉ đứng đây khi KHÔNG nửa phải (màn hẹp) trên md tấm
57
- ảnh đã mang logo + tên công ty rồi, lặp lạithừa. */}
56
+ {/* Logo + tên công ty chỉ đứng ĐÚNG một chỗ: góc trên trái. Nửa phải
57
+ hoa văn thuần, không mang chữ nhồi logo lên đó lặp. */}
58
58
  <Link
59
59
  href={ensureLocalizedPathname("/", locale)}
60
- className="flex md:hidden items-center gap-2 text-foreground font-black"
60
+ className="flex items-center gap-2 text-foreground font-black"
61
61
  >
62
62
  {branding?.logo ? (
63
63
  <Image
@@ -96,6 +96,65 @@ export function Auth({
96
96
  );
97
97
  }
98
98
 
99
+ /*
100
+ * Hoa văn khối lập phương đẳng cự (isometric) — hình học của tấm nền đăng nhập.
101
+ *
102
+ * Cạnh `a`; mỗi khối là một lục giác đều cộng ba nan từ tâm ra ba đỉnh, xếp kiểu
103
+ * tổ ong thì thành một mặt phẳng toàn khối hộp. Chọn hoa văn này vì nó CÓ NGHĨA
104
+ * với phần mềm quản trị: kiện hàng xếp kho, mô-đun ghép lại thành hệ thống —
105
+ * không phải mảng loang trang trí cho có.
106
+ *
107
+ * Toạ độ tính sẵn ở mức mô-đun (không phụ thuộc render) để khỏi tính lại mỗi lần
108
+ * vẽ và để sai số làm rời mạch hoa văn thì thấy ngay ở đây.
109
+ */
110
+ const CUBE_A = 60;
111
+ const CUBE_DX = 51.96; // a·√3/2 — nửa bề ngang lục giác
112
+ const CUBE_DY = 30; // a/2
113
+ const CUBE_TILE_W = 103.92; // a·√3
114
+ const CUBE_TILE_H = 180; // 3a — hai hàng tổ ong
115
+
116
+ function cubeAt(cx: number, cy: number) {
117
+ const { round } = Math;
118
+ const r = (n: number) => round(n * 100) / 100;
119
+ const [t, b] = [r(cy - CUBE_A), r(cy + CUBE_A)];
120
+ const [l, rt] = [r(cx - CUBE_DX), r(cx + CUBE_DX)];
121
+ const [up, dn] = [r(cy - CUBE_DY), r(cy + CUBE_DY)];
122
+ const x = r(cx);
123
+ return (
124
+ // Viền lục giác + ba nan tâm → ảo giác khối hộp.
125
+ `M${x} ${t}L${rt} ${up}L${rt} ${dn}L${x} ${b}L${l} ${dn}L${l} ${up}Z` +
126
+ `M${x} ${t}L${x} ${r(cy)}M${x} ${r(cy)}L${l} ${dn}M${x} ${r(cy)}L${rt} ${dn}`
127
+ );
128
+ }
129
+
130
+ function cubeTopFaceAt(cx: number, cy: number) {
131
+ return `M${cx} ${cy - CUBE_A}L${cx + CUBE_DX} ${cy - CUBE_DY}L${cx} ${cy}L${cx - CUBE_DX} ${cy - CUBE_DY}Z`;
132
+ }
133
+
134
+ // Ô lặp chứa hai hàng tổ ong; vẽ thêm tâm tràn ra ngoài mép để phần bị cắt được
135
+ // ô kế bên nối lại — thiếu chúng là hoa văn đứt thành từng mảng vuông.
136
+ const CUBE_TILE_CENTERS: Array<[number, number]> = [
137
+ [0, 0],
138
+ [CUBE_TILE_W, 0],
139
+ [0, CUBE_TILE_H],
140
+ [CUBE_TILE_W, CUBE_TILE_H],
141
+ [CUBE_TILE_W / 2, CUBE_TILE_H / 2],
142
+ [CUBE_TILE_W / 2, -CUBE_TILE_H / 2],
143
+ [CUBE_TILE_W / 2, CUBE_TILE_H * 1.5],
144
+ ];
145
+
146
+ // Vài mặt trên được tô đậm hơn, xếp thành hai vệt chéo song song — chỗ sáng chỗ
147
+ // tối làm hoa văn có nhịp, thay vì trải đều một mức xám chết.
148
+ const CUBE_HIGHLIGHTS: Array<[number, number, number]> = [
149
+ [311.77, 180, 0.1],
150
+ [363.73, 270, 0.16],
151
+ [415.69, 360, 0.12],
152
+ [467.65, 450, 0.07],
153
+ [155.88, 630, 0.09],
154
+ [207.85, 720, 0.14],
155
+ [259.8, 810, 0.08],
156
+ ];
157
+
99
158
  /**
100
159
  * Nền vector mặc định cho nửa phải khi app chưa có ảnh chụp thật.
101
160
  *
@@ -103,13 +162,8 @@ export function Auth({
103
162
  * thương hiệu là nền đổi theo, không app nào phải ship thêm ảnh, và không có
104
163
  * bitmap nào để phóng to bị vỡ.
105
164
  *
106
- * Hình khối cố ýmức kỹ thuật lưới như giấy kẻ ô mấy cung tròn quét từ
107
- * góc trên phải (tâm nằm NGOÀI khung nên thấy nét quét chứ không thành cái bia
108
- * bắn) — thay vì mấy vệt gradient loang: loang là thứ nhìn phát biết đồ máy
109
- * sinh, còn lưới + cung tròn là ngôn ngữ của bản vẽ kỹ thuật, hợp phần mềm quản
110
- * trị và không lỗi mốt.
111
- *
112
- * Mọi độ mờ đều dưới 20%: đây là nền, phần đọc được phải là logo + tên công ty.
165
+ * KHÔNG chữ hay logo đâylogo đã đứng góc trên trái, đặt thêm một cái
166
+ * nữa lên hoa văn lặp. Tấm này thuần hình.
113
167
  */
114
168
  export function AuthBrandPattern({ className }: { className?: string }) {
115
169
  return (
@@ -122,31 +176,45 @@ export function AuthBrandPattern({ className }: { className?: string }) {
122
176
  >
123
177
  <defs>
124
178
  <pattern
125
- id="goerp-auth-grid"
126
- width="44"
127
- height="44"
179
+ id="goerp-auth-cubes"
180
+ width={CUBE_TILE_W}
181
+ height={CUBE_TILE_H}
128
182
  patternUnits="userSpaceOnUse"
129
183
  >
130
- <path
131
- d="M44 0H0V44"
132
- stroke="currentColor"
133
- strokeOpacity="0.08"
134
- strokeWidth="1"
135
- />
184
+ {CUBE_TILE_CENTERS.map(([cx, cy]) => (
185
+ <path
186
+ key={`${cx}-${cy}`}
187
+ d={cubeAt(cx, cy)}
188
+ stroke="currentColor"
189
+ strokeOpacity="0.14"
190
+ strokeWidth="1.25"
191
+ />
192
+ ))}
136
193
  </pattern>
137
- <radialGradient id="goerp-auth-glow" cx="0.82" cy="0.1" r="0.8">
138
- <stop offset="0" stopColor="currentColor" stopOpacity="0.16" />
194
+ {/* Ánh sáng chếch từ trên phải: hoa văn phẳng đều thì nhìn như giấy dán
195
+ tường, vệt sáng mới ra chiều sâu. */}
196
+ <radialGradient id="goerp-auth-glow" cx="0.78" cy="0.12" r="0.85">
197
+ <stop offset="0" stopColor="currentColor" stopOpacity="0.14" />
139
198
  <stop offset="1" stopColor="currentColor" stopOpacity="0" />
140
199
  </radialGradient>
200
+ {/* Đáy tối dần để khối chân trang không bị hoa văn cắn vào. */}
201
+ <linearGradient id="goerp-auth-foot" x1="0" y1="0" x2="0" y2="1">
202
+ <stop offset="0.55" stopColor="#000" stopOpacity="0" />
203
+ <stop offset="1" stopColor="#000" stopOpacity="0.22" />
204
+ </linearGradient>
141
205
  </defs>
142
- <rect width="600" height="900" fill="url(#goerp-auth-grid)" />
143
- <rect width="600" height="900" fill="url(#goerp-auth-glow)" />
144
- <g stroke="currentColor" fill="none">
145
- <circle cx="640" cy="-60" r="280" strokeOpacity="0.16" />
146
- <circle cx="640" cy="-60" r="430" strokeOpacity="0.12" />
147
- <circle cx="640" cy="-60" r="600" strokeOpacity="0.08" />
148
- <circle cx="640" cy="-60" r="790" strokeOpacity="0.05" />
206
+ <rect width="600" height="900" fill="url(#goerp-auth-cubes)" />
207
+ <g fill="currentColor">
208
+ {CUBE_HIGHLIGHTS.map(([cx, cy, opacity]) => (
209
+ <path
210
+ key={`${cx}-${cy}`}
211
+ d={cubeTopFaceAt(cx, cy)}
212
+ fillOpacity={opacity}
213
+ />
214
+ ))}
149
215
  </g>
216
+ <rect width="600" height="900" fill="url(#goerp-auth-glow)" />
217
+ <rect width="600" height="900" fill="url(#goerp-auth-foot)" />
150
218
  </svg>
151
219
  );
152
220
  }
@@ -158,12 +226,13 @@ interface AuthImageProps extends ComponentProps<"div"> {
158
226
  }
159
227
 
160
228
  /**
161
- * Nửa phải trang đăng nhập.
229
+ * Nửa phải trang đăng nhập. THUẦN HÌNH — không logo, không tên công ty, không
230
+ * khẩu hiệu: thương hiệu đã đứng ở góc trên trái, in lại lần nữa ở đây là lặp và
231
+ * làm màn đăng nhập trông như trang quảng cáo.
162
232
  *
163
- * Hai trạng thái, cùng một khối logo + tên công ty ở dưới:
164
- * - CÓ ảnh: ảnh phủ kín, một lớp tối chuyển dần từ đáy lên để chữ đè lên còn
165
- * đọc được (nền ảnh không kiểm soát được ⇒ không có lớp này thì chữ trắng
166
- * rơi vào vùng sáng là mất hút).
233
+ * Hai trạng thái:
234
+ * - CÓ ảnh: ảnh chụp thật phủ kín, một lớp tối rất nhẹ để mép ảnh không chọi
235
+ * với cột nhập liệu bên trái.
167
236
  * - KHÔNG ảnh: nền `--primary` của tenant + `AuthBrandPattern`. Đây là mặc
168
237
  * định, KHÔNG phải trạng thái lỗi — thà một tấm màu thương hiệu sạch còn hơn
169
238
  * ảnh stock/AI dùng chung cho mọi khách.
@@ -177,7 +246,6 @@ export function AuthImage({
177
246
  imgSrc,
178
247
  ...props
179
248
  }: AuthImageProps) {
180
- const branding = useOptionalTenant()?.branding;
181
249
  const [imageBroken, setImageBroken] = useState(false);
182
250
  const hasPhoto = Boolean(imgSrc) && !imageBroken;
183
251
 
@@ -210,9 +278,11 @@ export function AuthImage({
210
278
  onError={() => setImageBroken(true)}
211
279
  className={cn("object-cover", imageClassName)}
212
280
  />
281
+ {/* Lớp tối nhẹ thôi: không còn chữ nào đè lên ảnh, nó chỉ để ảnh khỏi
282
+ chói hơn hẳn cột nhập liệu bên trái. */}
213
283
  <div
214
284
  aria-hidden
215
- className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/35 to-black/10"
285
+ className="absolute inset-0 bg-gradient-to-t from-black/45 via-black/10 to-transparent"
216
286
  />
217
287
  </>
218
288
  ) : (
@@ -226,55 +296,9 @@ export function AuthImage({
226
296
  aria-hidden
227
297
  className="absolute inset-0 hidden bg-black/55 dark:block"
228
298
  />
229
- <AuthBrandPattern className="text-primary-foreground dark:text-white" />
299
+ <AuthBrandPattern className="motion-safe:animate-status-in text-primary-foreground dark:text-white" />
230
300
  </>
231
301
  )}
232
-
233
- <div
234
- className={cn(
235
- "absolute inset-0 flex flex-col p-10",
236
- hasPhoto ? "justify-end" : "justify-center",
237
- )}
238
- >
239
- <div
240
- className={cn(
241
- "motion-safe:animate-status-in flex flex-col items-start gap-4",
242
- hasPhoto ? "text-white" : "text-primary-foreground dark:text-white",
243
- )}
244
- >
245
- {branding?.logo ? (
246
- // Logo khách phần lớn là nét sẫm trên nền trong suốt ⇒ đặt trên tấm
247
- // trắng đục để nó không tan vào ảnh tối hay nền màu.
248
- <span className="inline-flex rounded-xl bg-white p-3 shadow-sm">
249
- <Image
250
- src={branding.logo}
251
- alt={branding.companyName || "Logo"}
252
- height={40}
253
- width={160}
254
- className="h-10 w-auto object-contain"
255
- unoptimized
256
- />
257
- </span>
258
- ) : null}
259
- <div className="space-y-1.5">
260
- <p className="text-3xl font-semibold leading-tight tracking-tight text-balance">
261
- {branding?.companyName || "GoERP"}
262
- </p>
263
- {branding?.tagline ? (
264
- <p
265
- className={cn(
266
- "max-w-sm text-balance text-sm leading-relaxed",
267
- hasPhoto
268
- ? "text-white/80"
269
- : "text-primary-foreground/80 dark:text-white/80",
270
- )}
271
- >
272
- {branding.tagline}
273
- </p>
274
- ) : null}
275
- </div>
276
- </div>
277
- </div>
278
302
  </div>
279
303
  );
280
304
  }