@comitor/ui 0.6.0 → 0.6.2
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/dist/components/composite/form-field.js +5 -2
- package/dist/components/composite/form-field.js.map +1 -1
- package/dist/components/ui/field.js +6 -12
- package/dist/components/ui/field.js.map +1 -1
- package/dist/components/ui/input-group.js +21 -3
- package/dist/components/ui/input-group.js.map +1 -1
- package/dist/shell/comitor-lockup.js +32 -23
- package/dist/shell/comitor-lockup.js.map +1 -1
- package/dist/shell/comitor-logo.d.ts +4 -3
- package/dist/shell/comitor-logo.js +8 -1
- package/dist/shell/comitor-logo.js.map +1 -1
- package/package.json +1 -1
|
@@ -48,7 +48,7 @@ function FormField({
|
|
|
48
48
|
orientation,
|
|
49
49
|
className: cn(colSpanClasses[colSpan], className),
|
|
50
50
|
children: [
|
|
51
|
-
label && /* @__PURE__ */ jsxs(FieldLabel, { htmlFor: id,
|
|
51
|
+
label && /* @__PURE__ */ jsxs(FieldLabel, { htmlFor: id, children: [
|
|
52
52
|
label,
|
|
53
53
|
required && // Dấu * chỉ là tín hiệu thị giác — trạng thái bắt buộc đã nằm ở aria-required trên
|
|
54
54
|
// control, để trình đọc màn hình khỏi đọc thừa "sao".
|
|
@@ -61,7 +61,10 @@ function FormField({
|
|
|
61
61
|
hasError ? /* @__PURE__ */ jsxs(FieldError, { id: errorId, className: "flex items-center gap-1.5 text-xs animate-in fade-in slide-in-from-top-1", children: [
|
|
62
62
|
/* @__PURE__ */ jsx(AlertCircle, { "aria-hidden": "true", className: "size-3.5 shrink-0" }),
|
|
63
63
|
error
|
|
64
|
-
] }) : description &&
|
|
64
|
+
] }) : description && // `leading-normal` phải VIẾT LẠI sau `text-xs`: cùng cái bẫy tailwind-merge ở nhãn phía
|
|
65
|
+
// trên — `text-xs` xoá `leading-normal` trong base của `FieldDescription` và kéo dòng
|
|
66
|
+
// mô tả từ 1.5 xuống 1rem. Cỡ chữ nhỏ hơn base là CÓ Ý, chiều cao dòng thì không.
|
|
67
|
+
/* @__PURE__ */ jsx(FieldDescription, { id: descriptionId, className: "text-xs leading-normal", children: description })
|
|
65
68
|
] })
|
|
66
69
|
]
|
|
67
70
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/form-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\nimport { type ReactNode, useId } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from \"../ui/field.js\";\n\nexport type FormFieldColSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;\n\n/**\n * Thuộc tính a11y mà `FormField` tiêm xuống control con.\n *\n * Trải nguyên cụm này vào `<Input>`/`<Combobox>`/`<Select>` là control tự có id khớp nhãn,\n * tự báo lỗi và trạng thái bắt buộc — không phải nhớ từng thuộc tính ở mỗi trang.\n */\nexport interface FormFieldControlProps {\n id: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n \"aria-describedby\"?: string;\n}\n\nexport interface FormFieldProps {\n label?: ReactNode;\n /** Đánh dấu bắt buộc: vẽ dấu * và phát `aria-required` xuống control */\n required?: boolean;\n /** Mô tả/gợi ý dưới trường — bị ẩn khi có lỗi để không đọc chồng lời */\n description?: ReactNode;\n /** Thông báo lỗi; có giá trị là trường vào trạng thái invalid */\n error?: ReactNode;\n /** id của control — bỏ trống thì tự sinh bằng useId */\n htmlFor?: string;\n /** Số cột chiếm trong lưới 12 cột của `FormSection` (mặc định 12 = cả hàng) */\n colSpan?: FormFieldColSpan;\n orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n className?: string;\n /**\n * Control của trường. Dạng hàm sẽ nhận `FormFieldControlProps` để trải xuống —\n * dạng node thì app tự lo id/aria (dùng khi control đã tự quản, vd Checkbox có nhãn riêng).\n */\n children: ReactNode | ((control: FormFieldControlProps) => ReactNode);\n}\n\nconst colSpanClasses: Record<FormFieldColSpan, string> = {\n 1: \"col-span-12 md:col-span-1\",\n 2: \"col-span-12 md:col-span-2\",\n 3: \"col-span-12 sm:col-span-6 md:col-span-3\",\n 4: \"col-span-12 sm:col-span-6 md:col-span-4\",\n 5: \"col-span-12 sm:col-span-6 md:col-span-5\",\n 6: \"col-span-12 sm:col-span-6\",\n 7: \"col-span-12 md:col-span-7\",\n 8: \"col-span-12 md:col-span-8\",\n 9: \"col-span-12 md:col-span-9\",\n 10: \"col-span-12 md:col-span-10\",\n 11: \"col-span-12 md:col-span-11\",\n 12: \"col-span-12\"\n};\n\n/**\n * Một trường form: nhãn + control + mô tả/lỗi, đặt trong lưới 12 cột của `FormSection`.\n *\n * KHÔNG tự vẽ input. Nhãn và lỗi uỷ quyền xuống primitive `Field`, còn control do app chọn —\n * nếu composite tự dựng input thì mỗi kiểu control mới (combobox, date range, editor) lại phải\n * mở lại file này — một FormField \"đa năng\" ôm hết mọi loại control sẽ phình ra không có điểm dừng.\n */\nexport function FormField({\n label,\n required = false,\n description,\n error,\n htmlFor,\n colSpan = 12,\n orientation = \"vertical\",\n className,\n children\n}: FormFieldProps) {\n const generatedId = useId();\n const id = htmlFor ?? generatedId;\n const hasError = Boolean(error);\n const errorId = `${id}-error`;\n const descriptionId = `${id}-description`;\n\n const control: FormFieldControlProps = {\n id,\n \"aria-invalid\": hasError ? true : undefined,\n \"aria-required\": required ? true : undefined,\n \"aria-describedby\": hasError ? errorId : description ? descriptionId : undefined\n };\n\n return (\n <Field\n data-slot=\"form-field\"\n data-invalid={hasError || undefined}\n orientation={orientation}\n className={cn(colSpanClasses[colSpan], className)}\n >\n {label && (\n <FieldLabel htmlFor={id}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/form-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\nimport { type ReactNode, useId } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from \"../ui/field.js\";\n\nexport type FormFieldColSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;\n\n/**\n * Thuộc tính a11y mà `FormField` tiêm xuống control con.\n *\n * Trải nguyên cụm này vào `<Input>`/`<Combobox>`/`<Select>` là control tự có id khớp nhãn,\n * tự báo lỗi và trạng thái bắt buộc — không phải nhớ từng thuộc tính ở mỗi trang.\n */\nexport interface FormFieldControlProps {\n id: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n \"aria-describedby\"?: string;\n}\n\nexport interface FormFieldProps {\n label?: ReactNode;\n /** Đánh dấu bắt buộc: vẽ dấu * và phát `aria-required` xuống control */\n required?: boolean;\n /** Mô tả/gợi ý dưới trường — bị ẩn khi có lỗi để không đọc chồng lời */\n description?: ReactNode;\n /** Thông báo lỗi; có giá trị là trường vào trạng thái invalid */\n error?: ReactNode;\n /** id của control — bỏ trống thì tự sinh bằng useId */\n htmlFor?: string;\n /** Số cột chiếm trong lưới 12 cột của `FormSection` (mặc định 12 = cả hàng) */\n colSpan?: FormFieldColSpan;\n orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n className?: string;\n /**\n * Control của trường. Dạng hàm sẽ nhận `FormFieldControlProps` để trải xuống —\n * dạng node thì app tự lo id/aria (dùng khi control đã tự quản, vd Checkbox có nhãn riêng).\n */\n children: ReactNode | ((control: FormFieldControlProps) => ReactNode);\n}\n\nconst colSpanClasses: Record<FormFieldColSpan, string> = {\n 1: \"col-span-12 md:col-span-1\",\n 2: \"col-span-12 md:col-span-2\",\n 3: \"col-span-12 sm:col-span-6 md:col-span-3\",\n 4: \"col-span-12 sm:col-span-6 md:col-span-4\",\n 5: \"col-span-12 sm:col-span-6 md:col-span-5\",\n 6: \"col-span-12 sm:col-span-6\",\n 7: \"col-span-12 md:col-span-7\",\n 8: \"col-span-12 md:col-span-8\",\n 9: \"col-span-12 md:col-span-9\",\n 10: \"col-span-12 md:col-span-10\",\n 11: \"col-span-12 md:col-span-11\",\n 12: \"col-span-12\"\n};\n\n/**\n * Một trường form: nhãn + control + mô tả/lỗi, đặt trong lưới 12 cột của `FormSection`.\n *\n * KHÔNG tự vẽ input. Nhãn và lỗi uỷ quyền xuống primitive `Field`, còn control do app chọn —\n * nếu composite tự dựng input thì mỗi kiểu control mới (combobox, date range, editor) lại phải\n * mở lại file này — một FormField \"đa năng\" ôm hết mọi loại control sẽ phình ra không có điểm dừng.\n */\nexport function FormField({\n label,\n required = false,\n description,\n error,\n htmlFor,\n colSpan = 12,\n orientation = \"vertical\",\n className,\n children\n}: FormFieldProps) {\n const generatedId = useId();\n const id = htmlFor ?? generatedId;\n const hasError = Boolean(error);\n const errorId = `${id}-error`;\n const descriptionId = `${id}-description`;\n\n const control: FormFieldControlProps = {\n id,\n \"aria-invalid\": hasError ? true : undefined,\n \"aria-required\": required ? true : undefined,\n \"aria-describedby\": hasError ? errorId : description ? descriptionId : undefined\n };\n\n return (\n <Field\n data-slot=\"form-field\"\n data-invalid={hasError || undefined}\n orientation={orientation}\n className={cn(colSpanClasses[colSpan], className)}\n >\n {/*\n Nhãn KHÔNG truyền `className=\"text-sm\"`: `Label` bên dưới đã đặt sẵn `text-sm` nên chuỗi đó\n không mua được gì — mà lại làm mất HAI chiều cao dòng. `cn()` chạy qua tailwind-merge, ở đó\n một utility CỠ CHỮ xoá mọi `leading-*` đứng TRƯỚC nó: `text-sm` nằm cuối chuỗi cắt\n `leading-snug` của `FieldLabel`, rồi cắt tiếp `leading-none` trong base của `Label`. Kết quả\n là thẻ <label> không còn `leading-*` nào.\n */}\n {label && (\n <FieldLabel htmlFor={id}>\n {label}\n {required && (\n // Dấu * chỉ là tín hiệu thị giác — trạng thái bắt buộc đã nằm ở aria-required trên\n // control, để trình đọc màn hình khỏi đọc thừa \"sao\".\n // Bậc ink (7,15:1 light / 6,42:1 dark trên `--card`) chứ không `text-destructive`\n // (bậc NỀN, chỉ 3,53:1 trên `--card` ở dark ✗).\n <span aria-hidden=\"true\" className=\"text-destructive-ink\">\n *\n </span>\n )}\n </FieldLabel>\n )}\n\n <FieldContent>\n {typeof children === \"function\" ? children(control) : children}\n\n {hasError ? (\n <FieldError id={errorId} className=\"flex items-center gap-1.5 text-xs animate-in fade-in slide-in-from-top-1\">\n <AlertCircle aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n {error}\n </FieldError>\n ) : (\n description && (\n // `leading-normal` phải VIẾT LẠI sau `text-xs`: cùng cái bẫy tailwind-merge ở nhãn phía\n // trên — `text-xs` xoá `leading-normal` trong base của `FieldDescription` và kéo dòng\n // mô tả từ 1.5 xuống 1rem. Cỡ chữ nhỏ hơn base là CÓ Ý, chiều cao dòng thì không.\n <FieldDescription id={descriptionId} className=\"text-xs leading-normal\">\n {description}\n </FieldDescription>\n )\n )}\n </FieldContent>\n </Field>\n );\n}\n"],"mappings":";AAwGQ,SAOI,KAPJ;AAtGR,SAAS,mBAAmB;AAC5B,SAAyB,aAAa;AACtC,SAAS,UAAU;AACnB,SAAS,OAAO,cAAc,kBAAkB,YAAY,kBAAkB;AAsC9E,MAAM,iBAAmD;AAAA,EACvD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AASO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAmB;AACjB,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,WAAW;AACtB,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,gBAAgB,GAAG,EAAE;AAE3B,QAAM,UAAiC;AAAA,IACrC;AAAA,IACA,gBAAgB,WAAW,OAAO;AAAA,IAClC,iBAAiB,WAAW,OAAO;AAAA,IACnC,oBAAoB,WAAW,UAAU,cAAc,gBAAgB;AAAA,EACzE;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc,YAAY;AAAA,MAC1B;AAAA,MACA,WAAW,GAAG,eAAe,OAAO,GAAG,SAAS;AAAA,MAS/C;AAAA,iBACC,qBAAC,cAAW,SAAS,IAClB;AAAA;AAAA,UACA;AAAA;AAAA;AAAA;AAAA,UAKC,oBAAC,UAAK,eAAY,QAAO,WAAU,wBAAuB,eAE1D;AAAA,WAEJ;AAAA,QAGF,qBAAC,gBACE;AAAA,iBAAO,aAAa,aAAa,SAAS,OAAO,IAAI;AAAA,UAErD,WACC,qBAAC,cAAW,IAAI,SAAS,WAAU,4EACjC;AAAA,gCAAC,eAAY,eAAY,QAAO,WAAU,qBAAoB;AAAA,YAC7D;AAAA,aACH,IAEA;AAAA;AAAA;AAAA,UAIE,oBAAC,oBAAiB,IAAI,eAAe,WAAU,0BAC5C,uBACH;AAAA,WAGN;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -129,24 +129,18 @@ function FieldDescription({ className, ...props }) {
|
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
function FieldSeparator({ children, className, ...props }) {
|
|
132
|
-
return /* @__PURE__ */
|
|
132
|
+
return /* @__PURE__ */ jsx(
|
|
133
133
|
"div",
|
|
134
134
|
{
|
|
135
135
|
"data-slot": "field-separator",
|
|
136
136
|
"data-content": !!children,
|
|
137
137
|
className: cn("relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", className),
|
|
138
138
|
...props,
|
|
139
|
-
children: [
|
|
140
|
-
/* @__PURE__ */ jsx(Separator, { className: "
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
className: "bg-background text-muted-foreground relative mx-auto block w-fit px-2",
|
|
145
|
-
"data-slot": "field-separator-content",
|
|
146
|
-
children
|
|
147
|
-
}
|
|
148
|
-
)
|
|
149
|
-
]
|
|
139
|
+
children: children ? /* @__PURE__ */ jsxs("div", { className: "flex h-full items-center gap-2", children: [
|
|
140
|
+
/* @__PURE__ */ jsx(Separator, { className: "flex-1" }),
|
|
141
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground shrink-0", "data-slot": "field-separator-content", children }),
|
|
142
|
+
/* @__PURE__ */ jsx(Separator, { className: "flex-1" })
|
|
143
|
+
] }) : /* @__PURE__ */ jsx(Separator, { className: "absolute inset-0 top-1/2" })
|
|
150
144
|
}
|
|
151
145
|
);
|
|
152
146
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/field.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Label, type LabelProps } from \"./label.js\";\nimport { Separator } from \"./separator.js\";\n\nexport type FieldSetProps = React.ComponentProps<\"fieldset\">;\n\nfunction FieldSet({ className, ...props }: FieldSetProps) {\n return (\n <fieldset\n data-slot=\"field-set\"\n className={cn(\n \"flex flex-col gap-6\",\n \"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport interface FieldLegendProps extends React.ComponentProps<\"legend\"> {\n /** `legend` = tiêu đề nhóm cỡ base; `label` = cỡ sm như một nhãn thường. */\n variant?: \"legend\" | \"label\";\n}\n\nfunction FieldLegend({ className, variant = \"legend\", ...props }: FieldLegendProps) {\n return (\n <legend\n data-slot=\"field-legend\"\n data-variant={variant}\n className={cn(\"mb-3 font-medium\", \"data-[variant=legend]:text-base\", \"data-[variant=label]:text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport type FieldGroupProps = React.ComponentProps<\"div\">;\n\nfunction FieldGroup({ className, ...props }: FieldGroupProps) {\n return (\n <div\n data-slot=\"field-group\"\n className={cn(\n \"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4\",\n className\n )}\n {...props}\n />\n );\n}\n\n// `text-destructive-ink` (bậc CHỮ đỏ) chứ không `text-destructive` (bậc NỀN đỏ của nút xoá):\n// bậc nền chỉ 3,76:1 trên `--background` và 3,53:1 trên `--card` ở dark ✗. Ink: 7,15:1 (light) ·\n// 6,84:1 / 6,42:1 (dark) ✓. Cùng một lối với `form.tsx` và `composite/form-field.tsx`.\nconst fieldVariants = cva(\"group/field flex w-full data-[invalid=true]:text-destructive-ink\", {\n variants: {\n orientation: {\n vertical: [\"flex-col gap-2 [&>*]:w-full [&>.sr-only]:w-auto\"],\n horizontal: [\n \"flex-row items-center gap-3\",\n \"[&>[data-slot=field-label]]:flex-auto\",\n \"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\"\n ],\n responsive: [\n \"flex-col gap-2 [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:gap-3 @md/field-group:[&>*]:w-auto\",\n \"@md/field-group:[&>[data-slot=field-label]]:flex-auto\",\n \"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\"\n ]\n }\n },\n defaultVariants: {\n orientation: \"vertical\"\n }\n});\n\nexport interface FieldProps extends React.ComponentProps<\"div\">, VariantProps<typeof fieldVariants> {}\n\nfunction Field({ className, orientation = \"vertical\", ...props }: FieldProps) {\n return (\n <div\n role=\"group\"\n data-slot=\"field\"\n data-orientation={orientation}\n className={cn(fieldVariants({ orientation }), className)}\n {...props}\n />\n );\n}\n\nexport type FieldContentProps = React.ComponentProps<\"div\">;\n\nfunction FieldContent({ className, ...props }: FieldContentProps) {\n return (\n <div\n data-slot=\"field-content\"\n className={cn(\"group/field-content flex flex-1 flex-col gap-1.5 leading-snug\", className)}\n {...props}\n />\n );\n}\n\nexport type FieldLabelProps = LabelProps;\n\n/*\n * Thẻ chọn (label bọc cả một Field, kiểu radio-card): trạng thái ĐÃ CHỌN được kể bằng nền tint\n * `bg-primary/5` — chỉ khác nền trang cỡ 1,03:1 — nên VIỀN mới là tín hiệu thật, và nó chịu\n * ngưỡng 3:1 của WCAG 1.4.11. `border-primary` gold #E8B824 chỉ **1,86:1** với nền trang và\n * **1,80:1** với chính cái tint (light) ✗. `--primary-ink`: 6,51:1 với nền trang · 6,30:1 với\n * tint (light); 12,67:1 · 10,43:1 (dark) ✓. Tint giữ nguyên `--primary` vì đó là vai trò TÔ NỀN.\n */\nfunction FieldLabel({ className, ...props }: FieldLabelProps) {\n return (\n <Label\n data-slot=\"field-label\"\n className={cn(\n \"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50\",\n \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4\",\n \"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary-ink dark:has-data-[state=checked]:bg-primary/10\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type FieldTitleProps = React.ComponentProps<\"div\">;\n\nfunction FieldTitle({ className, ...props }: FieldTitleProps) {\n return (\n <div\n data-slot=\"field-label\"\n className={cn(\n \"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type FieldDescriptionProps = React.ComponentProps<\"p\">;\n\nfunction FieldDescription({ className, ...props }: FieldDescriptionProps) {\n return (\n <p\n data-slot=\"field-description\"\n className={cn(\n \"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance\",\n \"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5\",\n \"[&>a:hover]:text-link-ink-hover [&>a]:underline [&>a]:underline-offset-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport interface FieldSeparatorProps extends React.ComponentProps<\"div\"> {\n /** Có nội dung thì chữ nằm giữa đường kẻ (kiểu \"hoặc\"). */\n children?: React.ReactNode;\n}\n\nfunction FieldSeparator({ children, className, ...props }: FieldSeparatorProps) {\n return (\n <div\n data-slot=\"field-separator\"\n data-content={!!children}\n className={cn(\"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2\", className)}\n {...props}\n >\n <Separator className=\"absolute inset-0 top-1/2\" />\n {children && (\n <span\n className=\"bg-background text-muted-foreground relative mx-auto block w-fit px-2\"\n data-slot=\"field-separator-content\"\n >\n {children}\n </span>\n )}\n </div>\n );\n}\n\nexport interface FieldErrorProps extends React.ComponentProps<\"div\"> {\n /**\n * Danh sách lỗi (khớp shape của react-hook-form). Bỏ qua khi đã truyền `children`.\n * Một lỗi → in thẳng; nhiều lỗi → danh sách gạch đầu dòng.\n */\n errors?: Array<{ message?: string } | undefined>;\n}\n\nfunction FieldError({ className, children, errors, ...props }: FieldErrorProps) {\n const content = useMemo(() => {\n if (children) {\n return children;\n }\n\n if (!errors) {\n return null;\n }\n\n // Lọc sạch phần tử rỗng trước để dùng chính message làm key (tránh key theo index).\n const messages = errors.flatMap((error) => (error?.message ? [error.message] : []));\n\n if (messages.length === 0) {\n return null;\n }\n\n if (messages.length === 1) {\n return messages[0];\n }\n\n return (\n <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n {messages.map((message) => (\n <li key={message}>{message}</li>\n ))}\n </ul>\n );\n }, [children, errors]);\n\n if (!content) {\n return null;\n }\n\n return (\n <div\n role=\"alert\"\n data-slot=\"field-error\"\n className={cn(\"text-destructive-ink text-sm font-normal\", className)}\n {...props}\n >\n {content}\n </div>\n );\n}\n\nexport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSeparator,\n FieldSet,\n FieldTitle,\n fieldVariants\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/field.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Label, type LabelProps } from \"./label.js\";\nimport { Separator } from \"./separator.js\";\n\nexport type FieldSetProps = React.ComponentProps<\"fieldset\">;\n\nfunction FieldSet({ className, ...props }: FieldSetProps) {\n return (\n <fieldset\n data-slot=\"field-set\"\n className={cn(\n \"flex flex-col gap-6\",\n \"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport interface FieldLegendProps extends React.ComponentProps<\"legend\"> {\n /** `legend` = tiêu đề nhóm cỡ base; `label` = cỡ sm như một nhãn thường. */\n variant?: \"legend\" | \"label\";\n}\n\nfunction FieldLegend({ className, variant = \"legend\", ...props }: FieldLegendProps) {\n return (\n <legend\n data-slot=\"field-legend\"\n data-variant={variant}\n className={cn(\"mb-3 font-medium\", \"data-[variant=legend]:text-base\", \"data-[variant=label]:text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport type FieldGroupProps = React.ComponentProps<\"div\">;\n\nfunction FieldGroup({ className, ...props }: FieldGroupProps) {\n return (\n <div\n data-slot=\"field-group\"\n className={cn(\n \"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4\",\n className\n )}\n {...props}\n />\n );\n}\n\n// `text-destructive-ink` (bậc CHỮ đỏ) chứ không `text-destructive` (bậc NỀN đỏ của nút xoá):\n// bậc nền chỉ 3,76:1 trên `--background` và 3,53:1 trên `--card` ở dark ✗. Ink: 7,15:1 (light) ·\n// 6,84:1 / 6,42:1 (dark) ✓. Cùng một lối với `form.tsx` và `composite/form-field.tsx`.\nconst fieldVariants = cva(\"group/field flex w-full data-[invalid=true]:text-destructive-ink\", {\n variants: {\n orientation: {\n vertical: [\"flex-col gap-2 [&>*]:w-full [&>.sr-only]:w-auto\"],\n horizontal: [\n \"flex-row items-center gap-3\",\n \"[&>[data-slot=field-label]]:flex-auto\",\n \"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\"\n ],\n responsive: [\n \"flex-col gap-2 [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:gap-3 @md/field-group:[&>*]:w-auto\",\n \"@md/field-group:[&>[data-slot=field-label]]:flex-auto\",\n \"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\"\n ]\n }\n },\n defaultVariants: {\n orientation: \"vertical\"\n }\n});\n\nexport interface FieldProps extends React.ComponentProps<\"div\">, VariantProps<typeof fieldVariants> {}\n\nfunction Field({ className, orientation = \"vertical\", ...props }: FieldProps) {\n return (\n <div\n role=\"group\"\n data-slot=\"field\"\n data-orientation={orientation}\n className={cn(fieldVariants({ orientation }), className)}\n {...props}\n />\n );\n}\n\nexport type FieldContentProps = React.ComponentProps<\"div\">;\n\nfunction FieldContent({ className, ...props }: FieldContentProps) {\n return (\n <div\n data-slot=\"field-content\"\n className={cn(\"group/field-content flex flex-1 flex-col gap-1.5 leading-snug\", className)}\n {...props}\n />\n );\n}\n\nexport type FieldLabelProps = LabelProps;\n\n/*\n * Thẻ chọn (label bọc cả một Field, kiểu radio-card): trạng thái ĐÃ CHỌN được kể bằng nền tint\n * `bg-primary/5` — chỉ khác nền trang cỡ 1,03:1 — nên VIỀN mới là tín hiệu thật, và nó chịu\n * ngưỡng 3:1 của WCAG 1.4.11. `border-primary` gold #E8B824 chỉ **1,86:1** với nền trang và\n * **1,80:1** với chính cái tint (light) ✗. `--primary-ink`: 6,51:1 với nền trang · 6,30:1 với\n * tint (light); 12,67:1 · 10,43:1 (dark) ✓. Tint giữ nguyên `--primary` vì đó là vai trò TÔ NỀN.\n */\nfunction FieldLabel({ className, ...props }: FieldLabelProps) {\n return (\n <Label\n data-slot=\"field-label\"\n className={cn(\n \"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50\",\n \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4\",\n \"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary-ink dark:has-data-[state=checked]:bg-primary/10\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type FieldTitleProps = React.ComponentProps<\"div\">;\n\nfunction FieldTitle({ className, ...props }: FieldTitleProps) {\n return (\n <div\n data-slot=\"field-label\"\n className={cn(\n \"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type FieldDescriptionProps = React.ComponentProps<\"p\">;\n\nfunction FieldDescription({ className, ...props }: FieldDescriptionProps) {\n return (\n <p\n data-slot=\"field-description\"\n className={cn(\n \"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance\",\n \"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5\",\n \"[&>a:hover]:text-link-ink-hover [&>a]:underline [&>a]:underline-offset-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport interface FieldSeparatorProps extends React.ComponentProps<\"div\"> {\n /** Có nội dung thì chữ nằm giữa đường kẻ (kiểu \"hoặc\"). */\n children?: React.ReactNode;\n}\n\n/*\n * ─ CÓ CHỮ THÌ CẮT ĐƯỜNG KẺ, KHÔNG PHỦ MỘT MẢNG NỀN LÊN NÓ ─\n *\n * Kiểu quen thuộc (shadcn gốc, và comitor-ds) là một đường kẻ chạy suốt cộng một `<span>` tô\n * `bg-background` đè lên giữa để \"khoét\" chỗ cho chữ. Cách đó chỉ đúng khi thành phần đứng THẲNG\n * trên nền trang — mà chỗ dùng thật gần như luôn là bên trong một `Card`:\n *\n * --background #0E1116 ≠ --card #16181D (theme tối)\n *\n * nên mảng tô hiện nguyên hình một hình chữ nhật TỐI HƠN nằm giữa thẻ. Ở theme sáng hai token\n * cùng là #FFFFFF nên lỗi vô hình — đây đúng loại lỗi chỉ lộ ra sau khi đổi theme, và không có\n * cách nào chữa từ phía app vì `<span>` nằm bên trong gói.\n *\n * Bản này dựng chữ ở GIỮA hai đoạn kẻ (`flex` + `flex-1`), nên không cần biết phía sau là nền gì:\n * đúng trên `--background`, trên `--card`, trên `--popover`, trên nền tô của một `Alert`. Nhánh\n * KHÔNG có chữ giữ nguyên đường kẻ tuyệt đối như cũ.\n */\nfunction FieldSeparator({ children, className, ...props }: FieldSeparatorProps) {\n return (\n <div\n data-slot=\"field-separator\"\n data-content={!!children}\n className={cn(\"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2\", className)}\n {...props}\n >\n {children ? (\n <div className=\"flex h-full items-center gap-2\">\n <Separator className=\"flex-1\" />\n <span className=\"text-muted-foreground shrink-0\" data-slot=\"field-separator-content\">\n {children}\n </span>\n <Separator className=\"flex-1\" />\n </div>\n ) : (\n <Separator className=\"absolute inset-0 top-1/2\" />\n )}\n </div>\n );\n}\n\nexport interface FieldErrorProps extends React.ComponentProps<\"div\"> {\n /**\n * Danh sách lỗi (khớp shape của react-hook-form). Bỏ qua khi đã truyền `children`.\n * Một lỗi → in thẳng; nhiều lỗi → danh sách gạch đầu dòng.\n */\n errors?: Array<{ message?: string } | undefined>;\n}\n\nfunction FieldError({ className, children, errors, ...props }: FieldErrorProps) {\n const content = useMemo(() => {\n if (children) {\n return children;\n }\n\n if (!errors) {\n return null;\n }\n\n // Lọc sạch phần tử rỗng trước để dùng chính message làm key (tránh key theo index).\n const messages = errors.flatMap((error) => (error?.message ? [error.message] : []));\n\n if (messages.length === 0) {\n return null;\n }\n\n if (messages.length === 1) {\n return messages[0];\n }\n\n return (\n <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n {messages.map((message) => (\n <li key={message}>{message}</li>\n ))}\n </ul>\n );\n }, [children, errors]);\n\n if (!content) {\n return null;\n }\n\n return (\n <div\n role=\"alert\"\n data-slot=\"field-error\"\n className={cn(\"text-destructive-ink text-sm font-normal\", className)}\n {...props}\n >\n {content}\n </div>\n );\n}\n\nexport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSeparator,\n FieldSet,\n FieldTitle,\n fieldVariants\n};\n"],"mappings":";AAaI,cAoLI,YApLJ;AAXJ,SAAS,WAA8B;AAEvC,SAAS,eAAe;AACxB,SAAS,UAAU;AACnB,SAAS,aAA8B;AACvC,SAAS,iBAAiB;AAI1B,SAAS,SAAS,EAAE,WAAW,GAAG,MAAM,GAAkB;AACxD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAOA,SAAS,YAAY,EAAE,WAAW,UAAU,UAAU,GAAG,MAAM,GAAqB;AAClF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc;AAAA,MACd,WAAW,GAAG,oBAAoB,mCAAmC,gCAAgC,SAAS;AAAA,MAC7G,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAKA,MAAM,gBAAgB,IAAI,oEAAoE;AAAA,EAC5F,UAAU;AAAA,IACR,aAAa;AAAA,MACX,UAAU,CAAC,iDAAiD;AAAA,MAC5D,YAAY;AAAA,QACV;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,YAAY;AAAA,QACV;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,aAAa;AAAA,EACf;AACF,CAAC;AAID,SAAS,MAAM,EAAE,WAAW,cAAc,YAAY,GAAG,MAAM,GAAe;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,aAAU;AAAA,MACV,oBAAkB;AAAA,MAClB,WAAW,GAAG,cAAc,EAAE,YAAY,CAAC,GAAG,SAAS;AAAA,MACtD,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iEAAiE,SAAS;AAAA,MACvF,GAAG;AAAA;AAAA,EACN;AAEJ;AAWA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAwBA,SAAS,eAAe,EAAE,UAAU,WAAW,GAAG,MAAM,GAAwB;AAC9E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc,CAAC,CAAC;AAAA,MAChB,WAAW,GAAG,6EAA6E,SAAS;AAAA,MACnG,GAAG;AAAA,MAEH,qBACC,qBAAC,SAAI,WAAU,kCACb;AAAA,4BAAC,aAAU,WAAU,UAAS;AAAA,QAC9B,oBAAC,UAAK,WAAU,kCAAiC,aAAU,2BACxD,UACH;AAAA,QACA,oBAAC,aAAU,WAAU,UAAS;AAAA,SAChC,IAEA,oBAAC,aAAU,WAAU,4BAA2B;AAAA;AAAA,EAEpD;AAEJ;AAUA,SAAS,WAAW,EAAE,WAAW,UAAU,QAAQ,GAAG,MAAM,GAAoB;AAC9E,QAAM,UAAU,QAAQ,MAAM;AAC5B,QAAI,UAAU;AACZ,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AAGA,UAAM,WAAW,OAAO,QAAQ,CAAC,UAAW,OAAO,UAAU,CAAC,MAAM,OAAO,IAAI,CAAC,CAAE;AAElF,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO;AAAA,IACT;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,SAAS,CAAC;AAAA,IACnB;AAEA,WACE,oBAAC,QAAG,WAAU,sCACX,mBAAS,IAAI,CAAC,YACb,oBAAC,QAAkB,qBAAV,OAAkB,CAC5B,GACH;AAAA,EAEJ,GAAG,CAAC,UAAU,MAAM,CAAC;AAErB,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,aAAU;AAAA,MACV,WAAW,GAAG,4CAA4C,SAAS;AAAA,MAClE,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -19,10 +19,28 @@ function InputGroup({ className, ...props }) {
|
|
|
19
19
|
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
20
20
|
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
21
21
|
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
22
|
-
// Trạng thái focus.
|
|
23
|
-
|
|
24
|
-
//
|
|
22
|
+
// Trạng thái focus. KHÔNG có `ring-offset-*`: quầng focus phải DÍNH SÁT viền, đúng như
|
|
23
|
+
// `Input`, `Textarea` và `SelectTrigger` (cả ba đều `focus:ring-2 focus:ring-ring/50`,
|
|
24
|
+
// không offset). Bản trước có `ring-offset-2 ring-offset-background` nên ô nhập trong
|
|
25
|
+
// nhóm — điển hình là ô mật khẩu có nút hiện/ẩn — vẽ ra 2px nền TRANG chen giữa viền và
|
|
26
|
+
// quầng: trên nền `--card` (khác `--background` ở theme tối) đó là một vòng tối lộ rõ, và
|
|
27
|
+
// cạnh một ô nhập thường trong CÙNG một biểu mẫu thì thành hai kiểu focus khác nhau.
|
|
28
|
+
// `ring-offset-*` là quy ước của control KHÔNG phải ô nhập chữ (Button, Checkbox, Switch,
|
|
29
|
+
// Tabs…) — đừng kéo sang họ ô nhập.
|
|
30
|
+
"has-[[data-slot=input-group-control]:focus-visible]:border-primary-ink has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-2",
|
|
31
|
+
// Trạng thái lỗi — HAI đường, vì `aria-invalid` rơi vào hai chỗ khác nhau tuỳ cách dùng.
|
|
32
|
+
//
|
|
33
|
+
// 1. Tự lắp: `<InputGroup><InputGroupInput aria-invalid /></InputGroup>` → cờ nằm trên
|
|
34
|
+
// ô nhập CON, khớp bằng `has-[…]`.
|
|
35
|
+
// 2. Qua `<FormControl>` của `@comitor/ui/form`: Radix `Slot` dán `aria-invalid` lên
|
|
36
|
+
// CHÍNH thẻ `InputGroup` (nó là con trực tiếp của `FormControl`), không xuống tới ô
|
|
37
|
+
// nhập bên trong. `has-[…]` chỉ soi con cháu nên không bao giờ khớp — hệ quả là ô
|
|
38
|
+
// mật khẩu trong một `Form` KHÔNG BAO GIỜ đỏ viền khi validate hỏng, trong khi ô
|
|
39
|
+
// email ngay trên nó thì đỏ. Cùng họ với cạm bẫy `Combobox` đã ghi trong tài liệu.
|
|
40
|
+
//
|
|
41
|
+
// Giữ cả hai để không nhánh nào phải nhớ nhánh kia.
|
|
25
42
|
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive-ink dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
43
|
+
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive-ink dark:aria-invalid:ring-destructive/40",
|
|
26
44
|
className
|
|
27
45
|
),
|
|
28
46
|
...props
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/input-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps } from \"./button.js\";\nimport { Input } from \"./input.js\";\nimport { Textarea } from \"./textarea.js\";\n\n/*\n * ─ LỚP TÔ Ở DARK: `dark:bg-input-fill`, KHÔNG PHẢI `bg-input/30` (cùng lý do với Textarea) ─\n * Ở bảng mặc định lớp tô là thứ khoanh vùng nhóm ở dark; ở bảng tương phản cao nó trong suốt vì\n * `--input` bên đó là #7E8290 và tô 30% sẽ kéo chữ xuống dưới ngưỡng.\n *\n * Riêng ở đây cái sai còn kín hơn Textarea nếu ai đó viết thẳng `bg-input/30`: lớp tô nằm trên\n * thẻ CHA, còn chữ `--muted-foreground` nằm ở `InputGroupAddon` và ở placeholder của\n * `Input`/`Textarea` con — tức KHÁC chuỗi lớp, nên `scripts/check-contrast.mjs` không thấy được\n * cặp này (giới hạn §8.1). Con số nếu viết thẳng: 3,81 · 3,54 · 3,22:1 ✗ ở dark.\n */\nexport type InputGroupProps = React.ComponentProps<\"div\">;\n\nfunction InputGroup({ className, ...props }: InputGroupProps) {\n return (\n <div\n data-slot=\"input-group\"\n role=\"group\"\n className={cn(\n \"group/input-group border-input dark:bg-input-fill relative flex w-full items-center rounded-md border shadow-xs transition-colors duration-150 ease-out outline-none\",\n \"h-9 has-[>textarea]:h-auto\",\n\n // Biến thể theo vị trí addon.\n \"has-[>[data-align=inline-start]]:[&>input]:pl-2\",\n \"has-[>[data-align=inline-end]]:[&>input]:pr-2\",\n \"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3\",\n \"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3\",\n\n // Trạng thái focus.\n \"has-[[data-slot=input-group-control]:focus-visible]:border-primary-ink has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-background\",\n\n // Trạng thái lỗi.\n \"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive-ink dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40\",\n\n className\n )}\n {...props}\n />\n );\n}\n\nconst inputGroupAddonVariants = cva(\n \"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n {\n variants: {\n align: {\n \"inline-start\": \"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]\",\n \"inline-end\": \"order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]\",\n \"block-start\":\n \"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5\",\n \"block-end\": \"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5\"\n }\n },\n defaultVariants: {\n align: \"inline-start\"\n }\n }\n);\n\nexport interface InputGroupAddonProps\n extends React.ComponentProps<\"div\">,\n VariantProps<typeof inputGroupAddonVariants> {}\n\n/*\n * Bấm vào vùng addon thì dồn focus sang ô nhập bên trong (trừ khi bấm trúng một nút).\n * Đây là tiện ích chuột thuần tuý — bàn phím vẫn Tab thẳng vào input/button nên không\n * cần onKeyDown; addon không phải phần tử tương tác.\n */\nfunction InputGroupAddon({ className, align = \"inline-start\", ...props }: InputGroupAddonProps) {\n return (\n // biome-ignore lint/a11y/useKeyWithClickEvents: chỉ là lối tắt bằng chuột, bàn phím đã Tab thẳng tới control\n <div\n role=\"group\"\n data-slot=\"input-group-addon\"\n data-align={align}\n className={cn(inputGroupAddonVariants({ align }), className)}\n onClick={(event) => {\n if ((event.target as HTMLElement).closest(\"button\")) {\n return;\n }\n event.currentTarget.parentElement?.querySelector(\"input\")?.focus();\n }}\n {...props}\n />\n );\n}\n\nconst inputGroupButtonVariants = cva(\"text-sm shadow-none flex gap-2 items-center\", {\n variants: {\n size: {\n xs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n sm: \"h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5\",\n \"icon-xs\": \"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0\",\n \"icon-sm\": \"size-8 p-0 has-[>svg]:p-0\"\n }\n },\n defaultVariants: {\n size: \"xs\"\n }\n});\n\nexport type InputGroupButtonProps = Omit<ButtonProps, \"size\"> & VariantProps<typeof inputGroupButtonVariants>;\n\nfunction InputGroupButton({\n className,\n type = \"button\",\n variant = \"ghost\",\n size = \"xs\",\n ...props\n}: InputGroupButtonProps) {\n return (\n <Button\n type={type}\n data-size={size}\n variant={variant}\n className={cn(inputGroupButtonVariants({ size }), className)}\n {...props}\n />\n );\n}\n\nexport type InputGroupTextProps = React.ComponentProps<\"span\">;\n\nfunction InputGroupText({ className, ...props }: InputGroupTextProps) {\n return (\n <span\n className={cn(\n \"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type InputGroupInputProps = React.ComponentProps<\"input\">;\n\nfunction InputGroupInput({ className, ...props }: InputGroupInputProps) {\n return (\n <Input\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type InputGroupTextareaProps = React.ComponentProps<\"textarea\">;\n\nfunction InputGroupTextarea({ className, ...props }: InputGroupTextareaProps) {\n return (\n <Textarea\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n InputGroupText,\n InputGroupTextarea,\n inputGroupAddonVariants,\n inputGroupButtonVariants\n};\n"],"mappings":";AAuBI;AArBJ,SAAS,WAA8B;AAEvC,SAAS,UAAU;AACnB,SAAS,cAAgC;AACzC,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAczB,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,QAGA;AAAA;AAAA,QAGA;AAAA,QAEA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,MAAM,0BAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,OAAO;AAAA,QACL,gBAAgB;AAAA,QAChB,cAAc;AAAA,QACd,eACE;AAAA,QACF,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,OAAO;AAAA,IACT;AAAA,EACF;AACF;AAWA,SAAS,gBAAgB,EAAE,WAAW,QAAQ,gBAAgB,GAAG,MAAM,GAAyB;AAC9F;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAU;AAAA,QACV,cAAY;AAAA,QACZ,WAAW,GAAG,wBAAwB,EAAE,MAAM,CAAC,GAAG,SAAS;AAAA,QAC3D,SAAS,CAAC,UAAU;AAClB,cAAK,MAAM,OAAuB,QAAQ,QAAQ,GAAG;AACnD;AAAA,UACF;AACA,gBAAM,cAAc,eAAe,cAAc,OAAO,GAAG,MAAM;AAAA,QACnE;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA;AAEJ;AAEA,MAAM,2BAA2B,IAAI,+CAA+C;AAAA,EAClF,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,MAAM;AAAA,EACR;AACF,CAAC;AAID,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA,OAAO;AAAA,EACP,UAAU;AAAA,EACV,OAAO;AAAA,EACP,GAAG;AACL,GAA0B;AACxB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,aAAW;AAAA,MACX;AAAA,MACA,WAAW,GAAG,yBAAyB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MAC1D,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/input-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps } from \"./button.js\";\nimport { Input } from \"./input.js\";\nimport { Textarea } from \"./textarea.js\";\n\n/*\n * ─ LỚP TÔ Ở DARK: `dark:bg-input-fill`, KHÔNG PHẢI `bg-input/30` (cùng lý do với Textarea) ─\n * Ở bảng mặc định lớp tô là thứ khoanh vùng nhóm ở dark; ở bảng tương phản cao nó trong suốt vì\n * `--input` bên đó là #7E8290 và tô 30% sẽ kéo chữ xuống dưới ngưỡng.\n *\n * Riêng ở đây cái sai còn kín hơn Textarea nếu ai đó viết thẳng `bg-input/30`: lớp tô nằm trên\n * thẻ CHA, còn chữ `--muted-foreground` nằm ở `InputGroupAddon` và ở placeholder của\n * `Input`/`Textarea` con — tức KHÁC chuỗi lớp, nên `scripts/check-contrast.mjs` không thấy được\n * cặp này (giới hạn §8.1). Con số nếu viết thẳng: 3,81 · 3,54 · 3,22:1 ✗ ở dark.\n */\nexport type InputGroupProps = React.ComponentProps<\"div\">;\n\nfunction InputGroup({ className, ...props }: InputGroupProps) {\n return (\n <div\n data-slot=\"input-group\"\n role=\"group\"\n className={cn(\n \"group/input-group border-input dark:bg-input-fill relative flex w-full items-center rounded-md border shadow-xs transition-colors duration-150 ease-out outline-none\",\n \"h-9 has-[>textarea]:h-auto\",\n\n // Biến thể theo vị trí addon.\n \"has-[>[data-align=inline-start]]:[&>input]:pl-2\",\n \"has-[>[data-align=inline-end]]:[&>input]:pr-2\",\n \"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3\",\n \"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3\",\n\n // Trạng thái focus. KHÔNG có `ring-offset-*`: quầng focus phải DÍNH SÁT viền, đúng như\n // `Input`, `Textarea` và `SelectTrigger` (cả ba đều `focus:ring-2 focus:ring-ring/50`,\n // không offset). Bản trước có `ring-offset-2 ring-offset-background` nên ô nhập trong\n // nhóm — điển hình là ô mật khẩu có nút hiện/ẩn — vẽ ra 2px nền TRANG chen giữa viền và\n // quầng: trên nền `--card` (khác `--background` ở theme tối) đó là một vòng tối lộ rõ, và\n // cạnh một ô nhập thường trong CÙNG một biểu mẫu thì thành hai kiểu focus khác nhau.\n // `ring-offset-*` là quy ước của control KHÔNG phải ô nhập chữ (Button, Checkbox, Switch,\n // Tabs…) — đừng kéo sang họ ô nhập.\n \"has-[[data-slot=input-group-control]:focus-visible]:border-primary-ink has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-2\",\n\n // Trạng thái lỗi — HAI đường, vì `aria-invalid` rơi vào hai chỗ khác nhau tuỳ cách dùng.\n //\n // 1. Tự lắp: `<InputGroup><InputGroupInput aria-invalid /></InputGroup>` → cờ nằm trên\n // ô nhập CON, khớp bằng `has-[…]`.\n // 2. Qua `<FormControl>` của `@comitor/ui/form`: Radix `Slot` dán `aria-invalid` lên\n // CHÍNH thẻ `InputGroup` (nó là con trực tiếp của `FormControl`), không xuống tới ô\n // nhập bên trong. `has-[…]` chỉ soi con cháu nên không bao giờ khớp — hệ quả là ô\n // mật khẩu trong một `Form` KHÔNG BAO GIỜ đỏ viền khi validate hỏng, trong khi ô\n // email ngay trên nó thì đỏ. Cùng họ với cạm bẫy `Combobox` đã ghi trong tài liệu.\n //\n // Giữ cả hai để không nhánh nào phải nhớ nhánh kia.\n \"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive-ink dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40\",\n \"aria-invalid:ring-destructive/20 aria-invalid:border-destructive-ink dark:aria-invalid:ring-destructive/40\",\n\n className\n )}\n {...props}\n />\n );\n}\n\nconst inputGroupAddonVariants = cva(\n \"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n {\n variants: {\n align: {\n \"inline-start\": \"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]\",\n \"inline-end\": \"order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]\",\n \"block-start\":\n \"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5\",\n \"block-end\": \"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5\"\n }\n },\n defaultVariants: {\n align: \"inline-start\"\n }\n }\n);\n\nexport interface InputGroupAddonProps\n extends React.ComponentProps<\"div\">,\n VariantProps<typeof inputGroupAddonVariants> {}\n\n/*\n * Bấm vào vùng addon thì dồn focus sang ô nhập bên trong (trừ khi bấm trúng một nút).\n * Đây là tiện ích chuột thuần tuý — bàn phím vẫn Tab thẳng vào input/button nên không\n * cần onKeyDown; addon không phải phần tử tương tác.\n */\nfunction InputGroupAddon({ className, align = \"inline-start\", ...props }: InputGroupAddonProps) {\n return (\n // biome-ignore lint/a11y/useKeyWithClickEvents: chỉ là lối tắt bằng chuột, bàn phím đã Tab thẳng tới control\n <div\n role=\"group\"\n data-slot=\"input-group-addon\"\n data-align={align}\n className={cn(inputGroupAddonVariants({ align }), className)}\n onClick={(event) => {\n if ((event.target as HTMLElement).closest(\"button\")) {\n return;\n }\n event.currentTarget.parentElement?.querySelector(\"input\")?.focus();\n }}\n {...props}\n />\n );\n}\n\nconst inputGroupButtonVariants = cva(\"text-sm shadow-none flex gap-2 items-center\", {\n variants: {\n size: {\n xs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n sm: \"h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5\",\n \"icon-xs\": \"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0\",\n \"icon-sm\": \"size-8 p-0 has-[>svg]:p-0\"\n }\n },\n defaultVariants: {\n size: \"xs\"\n }\n});\n\nexport type InputGroupButtonProps = Omit<ButtonProps, \"size\"> & VariantProps<typeof inputGroupButtonVariants>;\n\nfunction InputGroupButton({\n className,\n type = \"button\",\n variant = \"ghost\",\n size = \"xs\",\n ...props\n}: InputGroupButtonProps) {\n return (\n <Button\n type={type}\n data-size={size}\n variant={variant}\n className={cn(inputGroupButtonVariants({ size }), className)}\n {...props}\n />\n );\n}\n\nexport type InputGroupTextProps = React.ComponentProps<\"span\">;\n\nfunction InputGroupText({ className, ...props }: InputGroupTextProps) {\n return (\n <span\n className={cn(\n \"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type InputGroupInputProps = React.ComponentProps<\"input\">;\n\nfunction InputGroupInput({ className, ...props }: InputGroupInputProps) {\n return (\n <Input\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type InputGroupTextareaProps = React.ComponentProps<\"textarea\">;\n\nfunction InputGroupTextarea({ className, ...props }: InputGroupTextareaProps) {\n return (\n <Textarea\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n InputGroupText,\n InputGroupTextarea,\n inputGroupAddonVariants,\n inputGroupButtonVariants\n};\n"],"mappings":";AAuBI;AArBJ,SAAS,WAA8B;AAEvC,SAAS,UAAU;AACnB,SAAS,cAAgC;AACzC,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAczB,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAUA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAaA;AAAA,QACA;AAAA,QAEA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,MAAM,0BAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,OAAO;AAAA,QACL,gBAAgB;AAAA,QAChB,cAAc;AAAA,QACd,eACE;AAAA,QACF,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,OAAO;AAAA,IACT;AAAA,EACF;AACF;AAWA,SAAS,gBAAgB,EAAE,WAAW,QAAQ,gBAAgB,GAAG,MAAM,GAAyB;AAC9F;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAU;AAAA,QACV,cAAY;AAAA,QACZ,WAAW,GAAG,wBAAwB,EAAE,MAAM,CAAC,GAAG,SAAS;AAAA,QAC3D,SAAS,CAAC,UAAU;AAClB,cAAK,MAAM,OAAuB,QAAQ,QAAQ,GAAG;AACnD;AAAA,UACF;AACA,gBAAM,cAAc,eAAe,cAAc,OAAO,GAAG,MAAM;AAAA,QACnE;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA;AAEJ;AAEA,MAAM,2BAA2B,IAAI,+CAA+C;AAAA,EAClF,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,MAAM;AAAA,EACR;AACF,CAAC;AAID,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA,OAAO;AAAA,EACP,UAAU;AAAA,EACV,OAAO;AAAA,EACP,GAAG;AACL,GAA0B;AACxB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,aAAW;AAAA,MACX;AAAA,MACA,WAAW,GAAG,yBAAyB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MAC1D,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { ComitorLogo } from "./comitor-logo.js";
|
|
4
4
|
const SIZE_MAP = {
|
|
@@ -6,40 +6,44 @@ const SIZE_MAP = {
|
|
|
6
6
|
icon: "w-9 h-9",
|
|
7
7
|
// 36px — vừa header cao 56px
|
|
8
8
|
gap: "gap-2.5",
|
|
9
|
+
// 10px · 0.278 × icon
|
|
9
10
|
wordmark: "text-sm",
|
|
10
|
-
// 14px
|
|
11
|
+
// 14px · 0.389 × icon
|
|
11
12
|
slogan: "text-[6px] tracking-[0.12em]",
|
|
12
|
-
//
|
|
13
|
+
// 6px — ngưỡng sàn đọc được, xem chú thích trên
|
|
13
14
|
ruleMargin: "my-0.5"
|
|
14
15
|
},
|
|
15
16
|
sm: {
|
|
16
17
|
icon: "w-12 h-12",
|
|
17
18
|
// 48px
|
|
18
|
-
gap: "gap-3",
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
gap: "gap-3.5",
|
|
20
|
+
// 14px · 0.292 × icon
|
|
21
|
+
wordmark: "text-xl",
|
|
22
|
+
// 20px · 0.417 × icon
|
|
23
|
+
slogan: "text-[7px] tracking-[0.10em]",
|
|
24
|
+
// 7px — slogan/wordmark = 0.897
|
|
23
25
|
ruleMargin: "my-1"
|
|
24
26
|
},
|
|
25
27
|
md: {
|
|
26
28
|
icon: "w-20 h-20",
|
|
27
29
|
// 80px
|
|
28
|
-
gap: "gap-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
gap: "gap-5.5",
|
|
31
|
+
// 22px · 0.275 × icon
|
|
32
|
+
wordmark: "text-3xl",
|
|
33
|
+
// 30px · 0.375 × icon
|
|
34
|
+
slogan: "text-[10px] tracking-[0.10em]",
|
|
35
|
+
// 10px — slogan/wordmark = 0.854
|
|
33
36
|
ruleMargin: "my-1.5"
|
|
34
37
|
},
|
|
35
38
|
lg: {
|
|
36
39
|
icon: "w-32 h-32",
|
|
37
40
|
// 128px
|
|
38
|
-
gap: "gap-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
gap: "gap-9",
|
|
42
|
+
// 36px · 0.281 × icon
|
|
43
|
+
wordmark: "text-5xl",
|
|
44
|
+
// 48px · 0.375 × icon
|
|
45
|
+
slogan: "text-[14px] tracking-[0.12em]",
|
|
46
|
+
// 14px — slogan/wordmark = 0.767
|
|
43
47
|
ruleMargin: "my-2"
|
|
44
48
|
}
|
|
45
49
|
};
|
|
@@ -63,17 +67,22 @@ function ComitorLockup({
|
|
|
63
67
|
role: "img",
|
|
64
68
|
"aria-label": `${wordmark} \u2014 ${slogan}`,
|
|
65
69
|
children: [
|
|
66
|
-
|
|
67
|
-
/* @__PURE__ */ jsx(ComitorLogo, { variant: "light", className: cn("shrink-0", s.icon, "dark:hidden") }),
|
|
68
|
-
/* @__PURE__ */ jsx(ComitorLogo, { variant: "dark", className: cn("shrink-0", s.icon, "hidden dark:block") })
|
|
69
|
-
] }) : /* @__PURE__ */ jsx(ComitorLogo, { variant: dark ? "dark" : "light", className: cn("shrink-0", s.icon) }),
|
|
70
|
+
/* @__PURE__ */ jsx(ComitorLogo, { variant, className: cn("shrink-0", s.icon) }),
|
|
70
71
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
71
72
|
/* @__PURE__ */ jsx(
|
|
72
73
|
"span",
|
|
73
74
|
{
|
|
74
75
|
className: cn(
|
|
75
|
-
"font-extrabold tracking-tight
|
|
76
|
+
"font-extrabold tracking-tight",
|
|
76
77
|
s.wordmark,
|
|
78
|
+
// `leading-none` PHẢI đứng SAU `s.wordmark`, không được gộp vào chuỗi đầu.
|
|
79
|
+
// `cn()` chạy qua tailwind-merge, mà ở đó một utility CỠ CHỮ (`text-base`, `text-2xl`…)
|
|
80
|
+
// xung đột với nhóm `leading` và XOÁ mọi `leading-*` đứng TRƯỚC nó — Tailwind coi
|
|
81
|
+
// `text-<size>` là cặp cỡ-chữ + chiều-cao-dòng. Viết `cn("… leading-none", s.wordmark)`
|
|
82
|
+
// thì `leading-none` bị cắt sạch, wordmark rơi về line-height mặc định (24px ở cỡ `sm`
|
|
83
|
+
// thay vì 16px): khối chữ cao thêm 8px, vạch vàng và slogan tụt xuống, lockup không
|
|
84
|
+
// còn khớp tỉ lệ brand book. Không có lỗi biên dịch nào — chỉ đo mới thấy.
|
|
85
|
+
"leading-none",
|
|
77
86
|
auto ? "text-ink dark:text-white" : dark ? "text-white" : "text-ink"
|
|
78
87
|
),
|
|
79
88
|
children: wordmark
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/comitor-lockup.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport { ComitorLogo } from \"./comitor-logo.js\";\n\n/**\n * Lockup ngang chính thức của Comitor — icon bên trái, wordmark + gạch vàng + slogan bên phải.\n * Giữ đúng tỉ lệ hình học của brand book để mọi bề mặt sản phẩm dùng chung một bố cục.\n *\n * <ComitorLockup variant=\"auto\" size=\"xs\" slogan=\"Tasks\" />\n *\n * • `variant` — light (nền trắng/kem), dark (nền mực/ấm), auto (tự đổi theo theme).\n * • `size` — xs | sm | md | lg: đổi cỡ icon và cỡ chữ cùng lúc.\n * • `wordmark` / `slogan` — đổi chữ khi cần cặp ngôn ngữ khác\n * (vd \"COMITOR TASKS\", \"THIỆN MINH / CÙNG NHAU TIẾN XA\").\n *\n * Không có \"use client\": thuần trình bày, RSC render được.\n */\nexport type ComitorLockupVariant = \"light\" | \"dark\" | \"auto\";\nexport type ComitorLockupSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface ComitorLockupProps {\n className?: string;\n variant?: ComitorLockupVariant;\n size?: ComitorLockupSize;\n wordmark?: string;\n slogan?: string;\n /** Class Tailwind GHI ĐÈ style slogan của preset cỡ chữ. Dùng khi cần căn bề rộng\n * slogan tuỳ biến cho khớp wordmark — vd header truyền cỡ nhỉnh hơn + tracking chặt\n * hơn để \"DESIGN SYSTEM\" rộng đúng bằng chữ \"COMITOR\". */\n sloganClassName?: string;\n style?: CSSProperties;\n}\n\n
|
|
1
|
+
{"version":3,"sources":["../../src/shell/comitor-lockup.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport { ComitorLogo } from \"./comitor-logo.js\";\n\n/**\n * Lockup ngang chính thức của Comitor — icon bên trái, wordmark + gạch vàng + slogan bên phải.\n * Giữ đúng tỉ lệ hình học của brand book để mọi bề mặt sản phẩm dùng chung một bố cục.\n *\n * <ComitorLockup variant=\"auto\" size=\"xs\" slogan=\"Tasks\" />\n *\n * • `variant` — light (nền trắng/kem), dark (nền mực/ấm), auto (tự đổi theo theme).\n * • `size` — xs | sm | md | lg: đổi cỡ icon và cỡ chữ cùng lúc.\n * • `wordmark` / `slogan` — đổi chữ khi cần cặp ngôn ngữ khác\n * (vd \"COMITOR TASKS\", \"THIỆN MINH / CÙNG NHAU TIẾN XA\").\n *\n * Không có \"use client\": thuần trình bày, RSC render được.\n */\nexport type ComitorLockupVariant = \"light\" | \"dark\" | \"auto\";\nexport type ComitorLockupSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface ComitorLockupProps {\n className?: string;\n variant?: ComitorLockupVariant;\n size?: ComitorLockupSize;\n wordmark?: string;\n slogan?: string;\n /** Class Tailwind GHI ĐÈ style slogan của preset cỡ chữ. Dùng khi cần căn bề rộng\n * slogan tuỳ biến cho khớp wordmark — vd header truyền cỡ nhỉnh hơn + tracking chặt\n * hơn để \"DESIGN SYSTEM\" rộng đúng bằng chữ \"COMITOR\". */\n sloganClassName?: string;\n style?: CSSProperties;\n}\n\n/*\n * ─ RÀNG BUỘC HÌNH HỌC, ĐO ĐƯỢC CHỨ KHÔNG PHẢI ƯỚC LƯỢNG ─\n *\n * Bố cục là một cột: wordmark trên, vạch vàng giữa, slogan dưới. Cột đó là `flex-col`, nên\n * **bề rộng vạch vàng = bề rộng của phần tử RỘNG NHẤT**. Từ đó ra ràng buộc số 1:\n *\n * bề rộng GLYPH của slogan ≤ bề rộng GLYPH của wordmark\n *\n * Vi phạm nó thì slogan mới là thứ kéo dài vạch, còn wordmark hụt vào trong — nhìn ra ngay là\n * \"chữ COMITOR bị nhỏ\", dù cỡ chữ chẳng liên quan gì tới nguyên nhân. Brand book giữ đúng ràng\n * buộc này: `comitor-horizontal-light.svg` vẽ vạch từ x=450 đến x=803.2, bằng chính bề rộng\n * \"COMITOR\" 72px.\n *\n * Bảng CŨ vi phạm ở CẢ BỐN cỡ với slogan mặc định \"Together Further\" (đo bằng Inter, chính font\n * của hệ; tỉ lệ = bề rộng slogan / bề rộng wordmark):\n *\n * xs 73,4 / 65,1 = 1,127 ✗ sm 85,6 / 74,4 = 1,151 ✗\n * md 122,3 / 111,6 = 1,096 ✗ lg 175,7 / 167,4 = 1,050 ✗\n *\n * Lý do kín: wordmark có 7 ký tự còn slogan có 16, nên slogan cần cỡ chữ NHỎ HƠN NHIỀU mới cân\n * được bề rộng — brand book để 25,64/72 = 0,356, bảng cũ để 0,42–0,44. Lỗi chỉ lộ ra khi slogan\n * là chuỗi mặc định DÀI; mọi chỗ dùng `xs` trong hệ đều truyền slogan NGẮN theo tên app\n * (\"Tài khoản\" 39,7px · \"Design System\" 58,6px) nên đều lọt, và không ai thấy gì suốt nhiều bản.\n *\n * Ràng buộc số 2 — bảng phải là một thang TỈ LỆ. Bảng cũ không phải: wordmark/icon tụt dần\n * 0,389 → 0,333 → 0,300 → 0,281 (chênh 38% giữa hai đầu), tức đổi `size` là đổi luôn BỐ CỤC chứ\n * không chỉ đổi cỡ. Bảng mới giữ 0,375–0,417, và gap theo icon ở mức ~0,28 đúng như brand book\n * (80/280 = 0,286).\n *\n * Số của bảng MỚI (đo cùng cách):\n *\n * sm 83,4 / 93,0 = 0,897 ✓ md 119,1 / 139,5 = 0,854 ✓ lg 171,2 / 223,2 = 0,767 ✓\n *\n * ⚠ `xs` CỐ Ý GIỮ NGUYÊN, và vẫn là cỡ duy nhất không thoả ràng buộc 1 với slogan mặc định.\n * Chữa được thì phải hoặc kéo slogan xuống ~5px (dưới ngưỡng đọc được), hoặc đẩy wordmark lên\n * 16px trên một icon 36px (wordmark/icon = 0,44 — chữ lấn át dấu hiệu). Cả hai đều tệ hơn cái\n * đang có, mà `xs` là cỡ dành cho header/thanh bên nơi slogan LUÔN là tên app viết ngắn. Ghi ra\n * đây để lần sau không ai \"sửa\" nó rồi làm hỏng lockup ở header.\n */\nconst SIZE_MAP: Record<\n ComitorLockupSize,\n { icon: string; gap: string; wordmark: string; slogan: string; ruleMargin: string }\n> = {\n xs: {\n icon: \"w-9 h-9\", // 36px — vừa header cao 56px\n gap: \"gap-2.5\", // 10px · 0.278 × icon\n wordmark: \"text-sm\", // 14px · 0.389 × icon\n slogan: \"text-[6px] tracking-[0.12em]\", // 6px — ngưỡng sàn đọc được, xem chú thích trên\n ruleMargin: \"my-0.5\"\n },\n sm: {\n icon: \"w-12 h-12\", // 48px\n gap: \"gap-3.5\", // 14px · 0.292 × icon\n wordmark: \"text-xl\", // 20px · 0.417 × icon\n slogan: \"text-[7px] tracking-[0.10em]\", // 7px — slogan/wordmark = 0.897\n ruleMargin: \"my-1\"\n },\n md: {\n icon: \"w-20 h-20\", // 80px\n gap: \"gap-5.5\", // 22px · 0.275 × icon\n wordmark: \"text-3xl\", // 30px · 0.375 × icon\n slogan: \"text-[10px] tracking-[0.10em]\", // 10px — slogan/wordmark = 0.854\n ruleMargin: \"my-1.5\"\n },\n lg: {\n icon: \"w-32 h-32\", // 128px\n gap: \"gap-9\", // 36px · 0.281 × icon\n wordmark: \"text-5xl\", // 48px · 0.375 × icon\n slogan: \"text-[14px] tracking-[0.12em]\", // 14px — slogan/wordmark = 0.767\n ruleMargin: \"my-2\"\n }\n};\n\nexport function ComitorLockup({\n className,\n variant = \"light\",\n size = \"md\",\n wordmark = \"COMITOR\",\n slogan = \"Together Further\",\n sloganClassName,\n style\n}: ComitorLockupProps) {\n const s = SIZE_MAP[size];\n const dark = variant === \"dark\";\n const auto = variant === \"auto\";\n\n return (\n <div\n className={cn(\"flex items-center\", s.gap, className)}\n style={style}\n role=\"img\"\n aria-label={`${wordmark} — ${slogan}`}\n >\n {/* Ba biến thể đi thẳng xuống `ComitorLogo` — từ 0.6.1 chính nó biết `auto`, nên không còn\n bản sao của cặp `dark:hidden` / `hidden dark:block` ở đây. */}\n <ComitorLogo variant={variant} className={cn(\"shrink-0\", s.icon)} />\n <div className=\"flex flex-col\">\n <span\n className={cn(\n \"font-extrabold tracking-tight\",\n s.wordmark,\n // `leading-none` PHẢI đứng SAU `s.wordmark`, không được gộp vào chuỗi đầu.\n // `cn()` chạy qua tailwind-merge, mà ở đó một utility CỠ CHỮ (`text-base`, `text-2xl`…)\n // xung đột với nhóm `leading` và XOÁ mọi `leading-*` đứng TRƯỚC nó — Tailwind coi\n // `text-<size>` là cặp cỡ-chữ + chiều-cao-dòng. Viết `cn(\"… leading-none\", s.wordmark)`\n // thì `leading-none` bị cắt sạch, wordmark rơi về line-height mặc định (24px ở cỡ `sm`\n // thay vì 16px): khối chữ cao thêm 8px, vạch vàng và slogan tụt xuống, lockup không\n // còn khớp tỉ lệ brand book. Không có lỗi biên dịch nào — chỉ đo mới thấy.\n \"leading-none\",\n auto ? \"text-ink dark:text-white\" : dark ? \"text-white\" : \"text-ink\"\n )}\n >\n {wordmark}\n </span>\n <span\n className={cn(\n \"h-px opacity-55\",\n s.ruleMargin,\n auto ? \"bg-gold-400 dark:bg-gold-200\" : dark ? \"bg-gold-200\" : \"bg-gold-400\"\n )}\n aria-hidden=\"true\"\n />\n <span\n className={cn(\n \"font-semibold uppercase\",\n sloganClassName ?? s.slogan,\n auto ? \"text-gold-500 dark:text-gold-200\" : dark ? \"text-gold-200\" : \"text-gold-500\"\n )}\n >\n {slogan}\n </span>\n </div>\n </div>\n );\n}\n"],"mappings":"AAgIM,cACA,YADA;AA/HN,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAsE5B,MAAM,WAGF;AAAA,EACF,IAAI;AAAA,IACF,MAAM;AAAA;AAAA,IACN,KAAK;AAAA;AAAA,IACL,UAAU;AAAA;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,YAAY;AAAA,EACd;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA;AAAA,IACN,KAAK;AAAA;AAAA,IACL,UAAU;AAAA;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,YAAY;AAAA,EACd;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA;AAAA,IACN,KAAK;AAAA;AAAA,IACL,UAAU;AAAA;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,YAAY;AAAA,EACd;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA;AAAA,IACN,KAAK;AAAA;AAAA,IACL,UAAU;AAAA;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,YAAY;AAAA,EACd;AACF;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,SAAS;AAAA,EACT;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,IAAI,SAAS,IAAI;AACvB,QAAM,OAAO,YAAY;AACzB,QAAM,OAAO,YAAY;AAEzB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,qBAAqB,EAAE,KAAK,SAAS;AAAA,MACnD;AAAA,MACA,MAAK;AAAA,MACL,cAAY,GAAG,QAAQ,WAAM,MAAM;AAAA,MAInC;AAAA,4BAAC,eAAY,SAAkB,WAAW,GAAG,YAAY,EAAE,IAAI,GAAG;AAAA,QAClE,qBAAC,SAAI,WAAU,iBACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAQF;AAAA,gBACA,OAAO,6BAA6B,OAAO,eAAe;AAAA,cAC5D;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,EAAE;AAAA,gBACF,OAAO,iCAAiC,OAAO,gBAAgB;AAAA,cACjE;AAAA,cACA,eAAY;AAAA;AAAA,UACd;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,mBAAmB,EAAE;AAAA,gBACrB,OAAO,qCAAqC,OAAO,kBAAkB;AAAA,cACvE;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -4,9 +4,10 @@ import { CSSProperties } from 'react';
|
|
|
4
4
|
/**
|
|
5
5
|
* Dấu hiệu nhận diện chính thức của Comitor (khối lập phương vàng + 3 hình người đồng hành).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Ba biến thể:
|
|
8
8
|
* • `variant="light"` — mặt trên vàng nhạt; dùng trên nền sáng (mặc định)
|
|
9
9
|
* • `variant="dark"` — mặt trên trắng; dùng trên nền tối / nền ấm
|
|
10
|
+
* • `variant="auto"` — tự đổi theo theme, giống `ComitorLockup variant="auto"`
|
|
10
11
|
*
|
|
11
12
|
* ⚠ **Ngoại lệ duy nhất của quy tắc "không hardcode màu"**: mã hex dưới đây là vector
|
|
12
13
|
* lấy nguyên từ file nguồn Figma của brand book. KHÔNG tokenize, KHÔNG đổi màu,
|
|
@@ -14,10 +15,10 @@ import { CSSProperties } from 'react';
|
|
|
14
15
|
*
|
|
15
16
|
* File này KHÔNG có "use client": logo là SVG tĩnh, để RSC render thẳng, không tốn JS phía client.
|
|
16
17
|
*/
|
|
17
|
-
type ComitorLogoVariant = "light" | "dark";
|
|
18
|
+
type ComitorLogoVariant = "light" | "dark" | "auto";
|
|
18
19
|
interface ComitorLogoProps {
|
|
19
20
|
className?: string;
|
|
20
|
-
/** Nền sáng dùng "light", nền tối/ấm dùng "dark". */
|
|
21
|
+
/** Nền sáng dùng "light", nền tối/ấm dùng "dark", theo theme dùng "auto". */
|
|
21
22
|
variant?: ComitorLogoVariant;
|
|
22
23
|
/** Nội dung <title> của SVG — cũng là aria-label. */
|
|
23
24
|
title?: string;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
2
3
|
function ComitorLogo({ className, variant = "light", title = "Comitor", style }) {
|
|
4
|
+
if (variant === "auto") {
|
|
5
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6
|
+
/* @__PURE__ */ jsx(ComitorLogo, { variant: "light", className: cn(className, "dark:hidden"), title, style }),
|
|
7
|
+
/* @__PURE__ */ jsx(ComitorLogo, { variant: "dark", className: cn(className, "hidden dark:block"), title, style })
|
|
8
|
+
] });
|
|
9
|
+
}
|
|
3
10
|
const dark = variant === "dark";
|
|
4
11
|
const c = dark ? {
|
|
5
12
|
accent: "#8A6800",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/comitor-logo.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\n\n/**\n * Dấu hiệu nhận diện chính thức của Comitor (khối lập phương vàng + 3 hình người đồng hành).\n *\n *
|
|
1
|
+
{"version":3,"sources":["../../src/shell/comitor-logo.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Dấu hiệu nhận diện chính thức của Comitor (khối lập phương vàng + 3 hình người đồng hành).\n *\n * Ba biến thể:\n * • `variant=\"light\"` — mặt trên vàng nhạt; dùng trên nền sáng (mặc định)\n * • `variant=\"dark\"` — mặt trên trắng; dùng trên nền tối / nền ấm\n * • `variant=\"auto\"` — tự đổi theo theme, giống `ComitorLockup variant=\"auto\"`\n *\n * ⚠ **Ngoại lệ duy nhất của quy tắc \"không hardcode màu\"**: mã hex dưới đây là vector\n * lấy nguyên từ file nguồn Figma của brand book. KHÔNG tokenize, KHÔNG đổi màu,\n * KHÔNG đơn giản hoá path — logo phải giống hệt nhau ở mọi sản phẩm.\n *\n * File này KHÔNG có \"use client\": logo là SVG tĩnh, để RSC render thẳng, không tốn JS phía client.\n */\nexport type ComitorLogoVariant = \"light\" | \"dark\" | \"auto\";\n\nexport interface ComitorLogoProps {\n className?: string;\n /** Nền sáng dùng \"light\", nền tối/ấm dùng \"dark\", theo theme dùng \"auto\". */\n variant?: ComitorLogoVariant;\n /** Nội dung <title> của SVG — cũng là aria-label. */\n title?: string;\n style?: CSSProperties;\n}\n\nexport function ComitorLogo({ className, variant = \"light\", title = \"Comitor\", style }: ComitorLogoProps) {\n /*\n * `auto` PHẢI vẽ hai bản rồi ẩn/hiện bằng `dark:`, không thể là một SVG đổi màu: mã hex nằm\n * thẳng trong `fill` của từng path (ngoại lệ có chủ ý của quy tắc \"không hardcode màu\" — vector\n * chép nguyên từ brand book), mà `dark:` không với tới thuộc tính `fill`. Đây đúng cách\n * `ComitorLockup variant=\"auto\"` đang làm với chính component này; đưa vào đây để app không phải\n * chép lại cặp `dark:hidden` / `hidden dark:block` ở mỗi chỗ dùng logo trần.\n */\n if (variant === \"auto\") {\n return (\n <>\n <ComitorLogo variant=\"light\" className={cn(className, \"dark:hidden\")} title={title} style={style} />\n <ComitorLogo variant=\"dark\" className={cn(className, \"hidden dark:block\")} title={title} style={style} />\n </>\n );\n }\n\n const dark = variant === \"dark\";\n const c = dark\n ? {\n accent: \"#8A6800\",\n bright: \"#F0CC50\",\n rich: \"#D4A820\",\n topFace: \"#FFFFFF\",\n sphereHi: \"#F0CC50\",\n sphereLo: \"#D4A820\"\n }\n : {\n accent: \"#7A5800\",\n bright: \"#E8B824\",\n rich: \"#B88A00\",\n topFace: \"#F5D060\",\n sphereHi: \"#E8B824\",\n sphereLo: \"#B88A00\"\n };\n\n return (\n <svg\n viewBox=\"0 0 256 256\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n style={style}\n role=\"img\"\n aria-label={title}\n >\n <title>{title}</title>\n {/* Bottom interior diamond */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 142.059L78.5642 170.543L128 209.32L177.344 170.543L128 142.059Z\"\n fill={c.accent}\n />\n {/* Left-front face */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M56.6951 86.8339L66.4352 149.502L91.1528 135.259L115.779 121.016V92.5315V64.0459L56.6951 86.8339Z\"\n fill={c.bright}\n />\n {/* Right-front face */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M189.473 149.502L196.733 88.3046L140.13 64.0459V92.5315V121.016L164.755 135.259L189.473 149.502Z\"\n fill={c.rich}\n />\n {/* Top face / outer sphere highlight */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 0C182.857 0 229.629 34.5499 247.822 82.975C223.747 95.6553 162.917 112.195 128 128C104.569 118.811 28.3934 89.4991 9.74011 78.9317C28.9448 32.6203 74.7053 0 128 0ZM170.544 81.5968L128 106.131L85.3639 81.5968L115.779 64.046L101.445 55.7761V55.6842L80.4941 43.6468L58.9921 31.15C61.2894 29.588 63.6785 28.0259 66.1594 26.4638L88.6719 39.5118C100.709 34.1824 113.941 31.2419 128 31.2419C145.183 31.2419 161.356 35.7445 175.415 43.6468L140.13 64.046L170.544 81.5968Z\"\n fill={c.topFace}\n />\n {/* Right sphere half */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M238.817 63.9541C249.752 82.7915 256 104.66 256 128C256 198.662 198.662 256 128 256L120.557 199.03L128 128C164.939 106.682 201.878 85.3641 238.817 63.9541ZM146.929 188.095L177.344 170.544V211.251C202.981 196.089 220.991 169.35 224.207 138.2L246.811 125.152C246.72 122.303 246.536 119.455 246.26 116.698L207.207 139.211L189.473 149.502V114.401L146.929 138.935V188.095Z\"\n fill={c.sphereHi}\n />\n {/* Left sphere half */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 256C57.2464 256 0 198.662 0 128C0 104.66 6.15656 82.7915 17.0912 63.9541C54.0302 85.3641 90.9691 106.682 128 128V256ZM108.979 188.095V138.935L66.4352 114.401V149.502L31.1501 129.103C31.5176 160.804 47.1387 188.83 70.9376 206.289V232.293C73.4184 233.672 75.9917 234.958 78.5644 236.152V211.251V170.544L108.979 188.095Z\"\n fill={c.sphereLo}\n />\n </svg>\n );\n}\n"],"mappings":"AAsCM,mBACE,KADF;AArCN,SAAS,UAAU;AA2BZ,SAAS,YAAY,EAAE,WAAW,UAAU,SAAS,QAAQ,WAAW,MAAM,GAAqB;AAQxG,MAAI,YAAY,QAAQ;AACtB,WACE,iCACE;AAAA,0BAAC,eAAY,SAAQ,SAAQ,WAAW,GAAG,WAAW,aAAa,GAAG,OAAc,OAAc;AAAA,MAClG,oBAAC,eAAY,SAAQ,QAAO,WAAW,GAAG,WAAW,mBAAmB,GAAG,OAAc,OAAc;AAAA,OACzG;AAAA,EAEJ;AAEA,QAAM,OAAO,YAAY;AACzB,QAAM,IAAI,OACN;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,IACA;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,OAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,MAAK;AAAA,MACL,cAAY;AAAA,MAEZ;AAAA,4BAAC,WAAO,iBAAM;AAAA,QAEd;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
package/package.json
CHANGED