diff --git a/.changeset/early-ligers-carry.md b/.changeset/early-ligers-carry.md
new file mode 100644
index 00000000..576f15b8
--- /dev/null
+++ b/.changeset/early-ligers-carry.md
@@ -0,0 +1,5 @@
+---
+"@premieroctet/next-admin": minor
+---
+
+feat: ui revamp, add theming capability
diff --git a/apps/docs/pages/docs/_meta.json b/apps/docs/pages/docs/_meta.json
index d4358b24..2ac783de 100644
--- a/apps/docs/pages/docs/_meta.json
+++ b/apps/docs/pages/docs/_meta.json
@@ -4,6 +4,7 @@
"getting-started": "Getting Started",
"api-docs": "API",
"i18n": "I18n",
+ "theming": "Theming",
"glossary": "Glossary",
"route": "Route name",
"edge-cases": "Edge cases"
diff --git a/apps/docs/pages/docs/i18n.mdx b/apps/docs/pages/docs/i18n.mdx
index cbe9d356..cf37c1e3 100644
--- a/apps/docs/pages/docs/i18n.mdx
+++ b/apps/docs/pages/docs/i18n.mdx
@@ -4,23 +4,25 @@ Next Admin supports i18n with the `translations` prop of the `NextAdmin` compone
The following keys are accepted:
-| Name | Description | Default value |
-| ------------------------------- | ------------------------------------------------------------------------------------- | ------------------------- |
-| list.header.add.label | The "Add" button in the list header | Add |
-| list.header.search.placeholder | The placeholder used in the search input | Search |
-| list.footer.indicator.showing | The "Showing from" text in the list indicator, e.g: Showing from 1 to 10 of 25 | Showing from |
-| list.footer.indicator.to | The "to" text in the list indicator, e.g: Showing from 1 to 10 of 25 | to |
-| list.footer.indicator.of | The "of" text in the list indicator, e.g: Showing from 1 to 10 of 25 | of |
-| list.row.actions.delete.label | The text in the delete button displayed at the end of each row | Delete |
-| list.empty.label | The text displayed when there is no row in the list | No \{\{resource\}\} found |
-| form.button.save.label | The text displayed in the form submit button | Submit |
-| form.button.delete.label | The text displayed in the form delete button | Delete |
-| form.widgets.file_upload.label | The text displayed in file upload widget to select a file | Choose a file |
-| form.widgets.file_upload.delete | The text displayed in file upload widget to delete the current file | Delete |
-| actions.label | The text displayed in the dropdown button for the actions list | Action |
-| actions.delete.label | The text displayed for the default delete action in the actions dropdown | Delete |
+| Name | Description | Default value |
+| -------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------- |
+| list.header.add.label | The "Add" button in the list header | Add |
+| list.header.search.placeholder | The placeholder used in the search input | Search |
+| list.footer.indicator.showing | The "Showing from" text in the list indicator, e.g: Showing from 1 to 10 of 25 | Showing from |
+| list.footer.indicator.to | The "to" text in the list indicator, e.g: Showing from 1 to 10 of 25 | to |
+| list.footer.indicator.of | The "of" text in the list indicator, e.g: Showing from 1 to 10 of 25 | of |
+| list.row.actions.delete.label | The text in the delete button displayed at the end of each row | Delete |
+| list.empty.label | The text displayed when there is no row in the list | No \{\{resource\}\} found |
+| form.button.save.label | The text displayed in the form submit button | Submit |
+| form.button.delete.label | The text displayed in the form delete button | Delete |
+| form.widgets.file_upload.label | The text displayed in file upload widget to select a file | Choose a file |
+| form.widgets.file_upload.drag_and_drop | The text displayed in file upload widget to indicate a drag & drop is possible | or drag and drop |
+| form.widgets.file_upload.delete | The text displayed in file upload widget to delete the current file | Delete |
+| actions.label | The text displayed in the dropdown button for the actions list | Action |
+| actions.delete.label | The text displayed for the default delete action in the actions dropdown | Delete |
There is two ways to translate these default keys, provide a function named `getMessages` inside the options or provide `translations` props to `NextAdmin` component.
+
> Note that the function way allows you to provide an object with a multiple level structure to translate the keys, while the `translations` props only allows you to provide a flat object (`form.widgets.file_upload.delete` ex.)
You can also pass your own set of translations. For example you can set a custom action name as a translation key, which will then be translated by the lib.
diff --git a/apps/docs/pages/docs/theming.mdx b/apps/docs/pages/docs/theming.mdx
new file mode 100644
index 00000000..a1de2593
--- /dev/null
+++ b/apps/docs/pages/docs/theming.mdx
@@ -0,0 +1,42 @@
+# Theming
+
+Next Admin comes with a default theme which currently consists of Tailwind's indigo color as a primary color.
+
+To use the default theme, you can import the generated CSS file from Next Admin:
+
+```js
+import "@premieroctet/next-admin/dist/styles.css";
+```
+
+However, you might want to override this primary color. It is possible to do so in your own Tailwind config file:
+
+```js
+module.exports = {
+ content: [
+ ...yourFiles,
+ "./node_modules/@premieroctet/next-admin/dist/**/*.js",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ nextadmin: {
+ primary: {
+ 50: "#f3f4f6",
+ 100: "#e7e9ed",
+ 200: "#c7cdd6",
+ 300: "#a7b1bf",
+ 400: "#67788f",
+ 500: "#27445f",
+ 600: "#24405a",
+ 700: "#1c334b",
+ 800: "#172940",
+ 900: "#0f1d2e",
+ },
+ },
+ },
+ },
+ },
+};
+```
+
+You will then need to import your own generated CSS file instead of the Next Admin's one.
diff --git a/apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx b/apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx
index 08c7eb85..526d2c3d 100644
--- a/apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx
+++ b/apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx
@@ -6,7 +6,6 @@ import Dashboard from "@/components/Dashboard";
import { options } from "@/options";
import { prisma } from "@/prisma";
import schema from "@/prisma/json-schema/json-schema.json";
-import "@premieroctet/next-admin/dist/styles.css";
export default async function AdminPage({
params,
diff --git a/apps/example/options.tsx b/apps/example/options.tsx
index 5f5b887e..e4ec87d9 100644
--- a/apps/example/options.tsx
+++ b/apps/example/options.tsx
@@ -47,8 +47,8 @@ export const options: NextAdminOptions = {
posts: "col-span-2 row-start-4",
role: "col-span-2 row-start-4",
birthDate: "col-span-3 row-start-5",
- avatar: "col-span-1 row-start-5",
- metadata: "col-span-4 row-start-6",
+ avatar: "col-span-4 row-start-6",
+ metadata: "col-span-4 row-start-7",
},
fields: {
email: {
diff --git a/apps/example/tailwind.config.js b/apps/example/tailwind.config.js
index 1c8eb2f2..8057b8ac 100644
--- a/apps/example/tailwind.config.js
+++ b/apps/example/tailwind.config.js
@@ -1,3 +1,5 @@
+const defaultColors = require("tailwindcss/colors");
+
/** @type {import('tailwindcss').Config} */
/* eslint-disable max-len */
module.exports = {
@@ -22,6 +24,9 @@ module.exports = {
},
extend: {
colors: {
+ nextadmin: {
+ primary: defaultColors.indigo,
+ },
// light mode
tremor: {
brand: {
diff --git a/packages/next-admin/src/components/ActionsDropdown.tsx b/packages/next-admin/src/components/ActionsDropdown.tsx
index da106f4a..c3511bc6 100644
--- a/packages/next-admin/src/components/ActionsDropdown.tsx
+++ b/packages/next-admin/src/components/ActionsDropdown.tsx
@@ -10,6 +10,8 @@ import {
} from "./radix/Dropdown";
import { useAction } from "../hooks/useAction";
import { useI18n } from "../context/I18nContext";
+import { Fragment, useState } from "react";
+import { Transition } from "@headlessui/react";
type Props = {
actions: ModelAction[];
@@ -26,13 +28,14 @@ const ActionsDropdown = ({
}: Props) => {
const { runAction } = useAction(resource, selectedIds);
const { t } = useI18n();
+ const [isOpen, setIsOpen] = useState(false);
const onActionClick = (action: ModelAction) => {
runAction(action);
};
return (
-
+
-
-
- {actions?.map((action) => {
- return (
- onActionClick(action)}
- >
- {t(action.title)}
-
- );
- })}
-
+
+
+
+ {actions?.map((action) => {
+ return (
+ onActionClick(action)}
+ >
+ {t(action.title)}
+
+ );
+ })}
+
+
);
diff --git a/packages/next-admin/src/components/Cell.tsx b/packages/next-admin/src/components/Cell.tsx
index 42a59387..291a90ae 100644
--- a/packages/next-admin/src/components/Cell.tsx
+++ b/packages/next-admin/src/components/Cell.tsx
@@ -35,7 +35,7 @@ export default function Cell({ cell, formatter }: Props) {
e.stopPropagation()}
href={`${basePath}/${cell.value.url}`}
- className="hover:underline cursor-pointer text-indigo-700 hover:text-indigo-900 font-semibold flex items-center gap-1"
+ className="hover:underline cursor-pointer text-nextadmin-primary-700 hover:text-nextadmin-primary-900 font-semibold flex items-center gap-1"
>
{cellValue}
@@ -74,7 +74,7 @@ export default function Cell({ cell, formatter }: Props) {
className={clsx(
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium",
cell
- ? "bg-indigo-50 text-indigo-500"
+ ? "bg-nextadmin-primary-50 text-nextadmin-primary-500"
: "bg-neutral-50 text-neutral-600"
)}
>
diff --git a/packages/next-admin/src/components/DataTable.tsx b/packages/next-admin/src/components/DataTable.tsx
index 01b83c7d..555ff9b9 100644
--- a/packages/next-admin/src/components/DataTable.tsx
+++ b/packages/next-admin/src/components/DataTable.tsx
@@ -45,17 +45,24 @@ export function DataTable({
const { basePath } = useConfig();
const { t } = useI18n();
- const columnsVisibility = columns.reduce((acc, column) => {
- // @ts-expect-error
- const key = column.accessorKey as Field;
- acc[key] = Object.keys(data[0]).includes(key);
- return acc;
- }, {} as Record, boolean>);
+ const columnsVisibility = columns.reduce(
+ (acc, column) => {
+ // @ts-expect-error
+ const key = column.accessorKey as Field;
+ acc[key] = Object.keys(data[0]).includes(key);
+ return acc;
+ },
+ {} as Record, boolean>
+ );
const modelIdProperty = resourcesIdProperty[resource];
const checkboxColumn: ColumnDef> = {
id: "__nextadmin-select-row",
header: ({ table }) => {
+ if (table.getRowModel().rows.length === 0) {
+ return null;
+ }
+
return (
{
evt.stopPropagation();
onDelete?.(row.original[idProperty].value as string | number);
@@ -124,9 +131,9 @@ export function DataTable({
});
return (
-