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 ( -
+
- + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { @@ -150,7 +157,7 @@ export function DataTable({ { window.location.href = `${basePath}/${resource.toLowerCase()}/${ row.original[modelIdProperty].value @@ -166,7 +173,10 @@ export function DataTable({ )) ) : ( - +
{t("list.empty.label", { resource })}
diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index e696da3b..69659d57 100644 --- a/packages/next-admin/src/components/Form.tsx +++ b/packages/next-admin/src/components/Form.tsx @@ -142,6 +142,8 @@ const Form = ({ if (result?.validation) { setValidation(result.validation); + } else { + setValidation(undefined); } if (result?.deleted) { @@ -303,7 +305,7 @@ const Form = ({ {...props} value={props.value ?? ""} className={clsx( - "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 px-2 disabled:opacity-50 disabled:bg-gray-200 disabled:cursor-not-allowed", + "block w-full transition-all duration-300 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-nextadmin-primary-600 sm:text-sm sm:leading-6 px-2 disabled:opacity-50 disabled:bg-gray-200 disabled:cursor-not-allowed", { "ring-red-600": rawErrors } )} /> diff --git a/packages/next-admin/src/components/ListHeader.tsx b/packages/next-admin/src/components/ListHeader.tsx index 1ce50af4..aaf9a8e8 100644 --- a/packages/next-admin/src/components/ListHeader.tsx +++ b/packages/next-admin/src/components/ListHeader.tsx @@ -68,7 +68,7 @@ export default function ListHeader({ onInput={onSearchChange} defaultValue={search} type="search" - className="px-3 py-1.5 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm focus-visible:outline focus-visible:outline-indigo-500 focus-visible:ring focus-visible:ring-indigo-500" + className="transition-all px-3 py-1.5 border border-gray-300 rounded-md shadow-sm focus:ring-nextadmin-primary-500 focus:border-nextadmin-primary-500 sm:text-sm focus-visible:outline focus-visible:outline-nextadmin-primary-500 focus-visible:ring-0 focus-visible:ring-nextadmin-primary-500" placeholder={t("list.header.search.placeholder")} /> diff --git a/packages/next-admin/src/components/Menu.tsx b/packages/next-admin/src/components/Menu.tsx index 0e5ce0a0..551db47c 100644 --- a/packages/next-admin/src/components/Menu.tsx +++ b/packages/next-admin/src/components/Menu.tsx @@ -58,8 +58,8 @@ export default function Menu({ href={item.href} className={clsx( item.current - ? "bg-gray-50 text-indigo-600" - : "text-gray-700 hover:text-indigo-600 hover:bg-gray-50", + ? "bg-gray-50 text-nextadmin-primary-600" + : "text-gray-700 hover:text-nextadmin-primary-600 hover:bg-gray-50", "group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold" )} > @@ -67,8 +67,8 @@ export default function Menu({