diff --git a/apps/api/src/locales/@vitnode/core/en.json b/apps/api/src/locales/@vitnode/core/en.json
index ebfd410cf..8a78cce11 100644
--- a/apps/api/src/locales/@vitnode/core/en.json
+++ b/apps/api/src/locales/@vitnode/core/en.json
@@ -103,6 +103,7 @@
"select_option": "Select an option",
"select_options": "Select options",
"select_language": "Select language",
+ "pick_color": "Pick a color",
"go_to_prev_page": "Go to previous page",
"go_to_next_page": "Go to next page",
"errors": {
diff --git a/apps/docs/content/docs/ui/color.mdx b/apps/docs/content/docs/ui/color.mdx
new file mode 100644
index 000000000..74771ac13
--- /dev/null
+++ b/apps/docs/content/docs/ui/color.mdx
@@ -0,0 +1,97 @@
+---
+title: Color
+description: Color picker field built on react-colorful that outputs an HSL string.
+---
+
+## Preview
+
+
+
+## Usage
+
+import { Tab, Tabs } from "fumadocs-ui/components/tabs";
+
+
+
+```ts
+import { z } from "zod";
+import { AutoForm } from "@vitnode/core/components/form/auto-form";
+import { AutoFormColor } from "@vitnode/core/components/form/fields/color";
+```
+
+```ts
+const formSchema = z.object({
+ color: z.string().default("hsl(240, 80%, 60%)"),
+});
+```
+
+```tsx
+ ,
+ },
+ ]}
+/>
+```
+
+
+
+
+```ts
+import { ColorPicker } from "@vitnode/core/components/ui/color-picker";
+```
+
+The `ColorPicker` is controlled - pass the current `value` (an HSL string) and
+handle `onChange`, which receives the new HSL string:
+
+```tsx
+const [color, setColor] = useState("hsl(240, 80%, 60%)");
+
+;
+```
+
+
+
+
+
+ Dragging the picker produces an HSL string such as `hsl(240, 80%, 60%)`. Below
+ the picker is a text field where you can paste or type a color in any CSS
+ format - hex (`#22c55e`), `rgb(...)`, `hsl(...)`, or a named color - so the
+ stored value is whatever you enter. The picker is powered by
+ [react-colorful](https://github.com/omgovich/react-colorful) and is
+ lazy-loaded, so its bundle only ships once the popover is opened.
+
+
+## Props
+
+import { TypeTable } from "fumadocs-ui/components/type-table";
+
+
diff --git a/apps/docs/content/docs/ui/meta.json b/apps/docs/content/docs/ui/meta.json
index 288445241..3918c4b3a 100644
--- a/apps/docs/content/docs/ui/meta.json
+++ b/apps/docs/content/docs/ui/meta.json
@@ -15,6 +15,7 @@
"---Forms---",
"auto-form",
"checkbox",
+ "color",
"combobox",
"editor",
"input",
diff --git a/apps/docs/migrations/0009_modern_eternals.sql b/apps/docs/migrations/0009_modern_eternals.sql
new file mode 100644
index 000000000..c652e96dc
--- /dev/null
+++ b/apps/docs/migrations/0009_modern_eternals.sql
@@ -0,0 +1 @@
+ALTER TABLE "core_roles" ALTER COLUMN "color" SET DATA TYPE varchar(50);
\ No newline at end of file
diff --git a/apps/docs/migrations/meta/0009_snapshot.json b/apps/docs/migrations/meta/0009_snapshot.json
new file mode 100644
index 000000000..7f1768915
--- /dev/null
+++ b/apps/docs/migrations/meta/0009_snapshot.json
@@ -0,0 +1,1880 @@
+{
+ "id": "7fd0cead-725d-42f5-bc00-7e1d8954ea75",
+ "prevId": "4829773c-6615-48c5-bebb-5ece6e9a38a5",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.core_admin_permissions": {
+ "name": "core_admin_permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "roleId": {
+ "name": "roleId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "protected": {
+ "name": "protected",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"unrestricted\":false,\"permissions\":[]}'::jsonb"
+ }
+ },
+ "indexes": {
+ "core_admin_permissions_role_id_idx": {
+ "name": "core_admin_permissions_role_id_idx",
+ "columns": [
+ {
+ "expression": "roleId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "core_admin_permissions_user_id_idx": {
+ "name": "core_admin_permissions_user_id_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_admin_permissions_roleId_core_roles_id_fk": {
+ "name": "core_admin_permissions_roleId_core_roles_id_fk",
+ "tableFrom": "core_admin_permissions",
+ "tableTo": "core_roles",
+ "columnsFrom": [
+ "roleId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "core_admin_permissions_userId_core_users_id_fk": {
+ "name": "core_admin_permissions_userId_core_users_id_fk",
+ "tableFrom": "core_admin_permissions",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_admin_sessions": {
+ "name": "core_admin_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "lastSeen": {
+ "name": "lastSeen",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deviceId": {
+ "name": "deviceId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "core_admin_sessions_token_idx": {
+ "name": "core_admin_sessions_token_idx",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "core_admin_sessions_user_id_idx": {
+ "name": "core_admin_sessions_user_id_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_admin_sessions_userId_core_users_id_fk": {
+ "name": "core_admin_sessions_userId_core_users_id_fk",
+ "tableFrom": "core_admin_sessions",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk": {
+ "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk",
+ "tableFrom": "core_admin_sessions",
+ "tableTo": "core_sessions_known_devices",
+ "columnsFrom": [
+ "deviceId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "core_admin_sessions_token_unique": {
+ "name": "core_admin_sessions_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_cron": {
+ "name": "core_cron",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lastRun": {
+ "name": "lastRun",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "pluginId": {
+ "name": "pluginId",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "module": {
+ "name": "module",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "nextRun": {
+ "name": "nextRun",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schedule": {
+ "name": "schedule",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_files": {
+ "name": "core_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "folder": {
+ "name": "folder",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mimeType": {
+ "name": "mimeType",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pluginId": {
+ "name": "pluginId",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "core_files_user_id_idx": {
+ "name": "core_files_user_id_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_files_userId_core_users_id_fk": {
+ "name": "core_files_userId_core_users_id_fk",
+ "tableFrom": "core_files",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "core_files_key_unique": {
+ "name": "core_files_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_languages": {
+ "name": "core_languages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'UTC'"
+ },
+ "protected": {
+ "name": "protected",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "default": {
+ "name": "default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "time24": {
+ "name": "time24",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {
+ "core_languages_code_idx": {
+ "name": "core_languages_code_idx",
+ "columns": [
+ {
+ "expression": "code",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "core_languages_name_idx": {
+ "name": "core_languages_name_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "core_languages_code_unique": {
+ "name": "core_languages_code_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "code"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_languages_words": {
+ "name": "core_languages_words",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "languageCode": {
+ "name": "languageCode",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pluginCode": {
+ "name": "pluginCode",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "itemId": {
+ "name": "itemId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tableName": {
+ "name": "tableName",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variable": {
+ "name": "variable",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "core_languages_words_lang_code_idx": {
+ "name": "core_languages_words_lang_code_idx",
+ "columns": [
+ {
+ "expression": "languageCode",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_languages_words_languageCode_core_languages_code_fk": {
+ "name": "core_languages_words_languageCode_core_languages_code_fk",
+ "tableFrom": "core_languages_words",
+ "tableTo": "core_languages",
+ "columnsFrom": [
+ "languageCode"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_logs": {
+ "name": "core_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "pluginId": {
+ "name": "pluginId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "ipAddress": {
+ "name": "ipAddress",
+ "type": "varchar(45)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "method": {
+ "name": "method",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'GET'"
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'localhost'"
+ },
+ "userAgent": {
+ "name": "userAgent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "statusCode": {
+ "name": "statusCode",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 500
+ },
+ "userId": {
+ "name": "userId",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "test123": {
+ "name": "test123",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "core_logs_userId_core_users_id_fk": {
+ "name": "core_logs_userId_core_users_id_fk",
+ "tableFrom": "core_logs",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_moderators_permissions": {
+ "name": "core_moderators_permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "roleId": {
+ "name": "roleId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "protected": {
+ "name": "protected",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"unrestricted\":false,\"permissions\":[]}'::jsonb"
+ }
+ },
+ "indexes": {
+ "core_moderators_permissions_role_id_idx": {
+ "name": "core_moderators_permissions_role_id_idx",
+ "columns": [
+ {
+ "expression": "roleId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "core_moderators_permissions_user_id_idx": {
+ "name": "core_moderators_permissions_user_id_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_moderators_permissions_roleId_core_roles_id_fk": {
+ "name": "core_moderators_permissions_roleId_core_roles_id_fk",
+ "tableFrom": "core_moderators_permissions",
+ "tableTo": "core_roles",
+ "columnsFrom": [
+ "roleId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "core_moderators_permissions_userId_core_users_id_fk": {
+ "name": "core_moderators_permissions_userId_core_users_id_fk",
+ "tableFrom": "core_moderators_permissions",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_queue": {
+ "name": "core_queue",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "pluginId": {
+ "name": "pluginId",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "queue": {
+ "name": "queue",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "maxAttempts": {
+ "name": "maxAttempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "availableAt": {
+ "name": "availableAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "reservedAt": {
+ "name": "reservedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lastError": {
+ "name": "lastError",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completedAt": {
+ "name": "completedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "core_queue_status_available_at_idx": {
+ "name": "core_queue_status_available_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "availableAt",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_roles": {
+ "name": "core_roles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "protected": {
+ "name": "protected",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "default": {
+ "name": "default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "root": {
+ "name": "root",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "guest": {
+ "name": "guest",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_sessions": {
+ "name": "core_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deviceId": {
+ "name": "deviceId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "core_sessions_user_id_idx": {
+ "name": "core_sessions_user_id_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_sessions_userId_core_users_id_fk": {
+ "name": "core_sessions_userId_core_users_id_fk",
+ "tableFrom": "core_sessions",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "core_sessions_deviceId_core_sessions_known_devices_id_fk": {
+ "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk",
+ "tableFrom": "core_sessions",
+ "tableTo": "core_sessions_known_devices",
+ "columnsFrom": [
+ "deviceId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "core_sessions_token_unique": {
+ "name": "core_sessions_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_sessions_known_devices": {
+ "name": "core_sessions_known_devices",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "publicId": {
+ "name": "publicId",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ipAddress": {
+ "name": "ipAddress",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userAgent": {
+ "name": "userAgent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "lastSeen": {
+ "name": "lastSeen",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "core_sessions_known_devices_ip_address_idx": {
+ "name": "core_sessions_known_devices_ip_address_idx",
+ "columns": [
+ {
+ "expression": "ipAddress",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "core_sessions_known_devices_publicId_unique": {
+ "name": "core_sessions_known_devices_publicId_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "publicId"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_users": {
+ "name": "core_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "nameCode": {
+ "name": "nameCode",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "newsletter": {
+ "name": "newsletter",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "avatarColor": {
+ "name": "avatarColor",
+ "type": "varchar(6)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "roleId": {
+ "name": "roleId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "birthday": {
+ "name": "birthday",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ipAddress": {
+ "name": "ipAddress",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "language": {
+ "name": "language",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'en'"
+ }
+ },
+ "indexes": {
+ "core_users_name_code_idx": {
+ "name": "core_users_name_code_idx",
+ "columns": [
+ {
+ "expression": "nameCode",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "core_users_name_idx": {
+ "name": "core_users_name_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "core_users_email_idx": {
+ "name": "core_users_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_users_roleId_core_roles_id_fk": {
+ "name": "core_users_roleId_core_roles_id_fk",
+ "tableFrom": "core_users",
+ "tableTo": "core_roles",
+ "columnsFrom": [
+ "roleId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "core_users_language_core_languages_code_fk": {
+ "name": "core_users_language_core_languages_code_fk",
+ "tableFrom": "core_users",
+ "tableTo": "core_languages",
+ "columnsFrom": [
+ "language"
+ ],
+ "columnsTo": [
+ "code"
+ ],
+ "onDelete": "set default",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "core_users_nameCode_unique": {
+ "name": "core_users_nameCode_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "nameCode"
+ ]
+ },
+ "core_users_name_unique": {
+ "name": "core_users_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ },
+ "core_users_email_unique": {
+ "name": "core_users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_users_confirm_emails": {
+ "name": "core_users_confirm_emails",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ipAddress": {
+ "name": "ipAddress",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "core_users_confirm_emails_userId_core_users_id_fk": {
+ "name": "core_users_confirm_emails_userId_core_users_id_fk",
+ "tableFrom": "core_users_confirm_emails",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "core_users_confirm_emails_token_unique": {
+ "name": "core_users_confirm_emails_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_users_forgot_password": {
+ "name": "core_users_forgot_password",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ipAddress": {
+ "name": "ipAddress",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "core_users_forgot_password_userId_core_users_id_fk": {
+ "name": "core_users_forgot_password_userId_core_users_id_fk",
+ "tableFrom": "core_users_forgot_password",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "core_users_forgot_password_userId_unique": {
+ "name": "core_users_forgot_password_userId_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "userId"
+ ]
+ },
+ "core_users_forgot_password_token_unique": {
+ "name": "core_users_forgot_password_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_users_secondary_roles": {
+ "name": "core_users_secondary_roles",
+ "schema": "",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "roleId": {
+ "name": "roleId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "core_users_secondary_roles_user_id_idx": {
+ "name": "core_users_secondary_roles_user_id_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "core_users_secondary_roles_role_id_idx": {
+ "name": "core_users_secondary_roles_role_id_idx",
+ "columns": [
+ {
+ "expression": "roleId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_users_secondary_roles_userId_core_users_id_fk": {
+ "name": "core_users_secondary_roles_userId_core_users_id_fk",
+ "tableFrom": "core_users_secondary_roles",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "core_users_secondary_roles_roleId_core_roles_id_fk": {
+ "name": "core_users_secondary_roles_roleId_core_roles_id_fk",
+ "tableFrom": "core_users_secondary_roles",
+ "tableTo": "core_roles",
+ "columnsFrom": [
+ "roleId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "core_users_secondary_roles_userId_roleId_pk": {
+ "name": "core_users_secondary_roles_userId_roleId_pk",
+ "columns": [
+ "userId",
+ "roleId"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.core_users_sso": {
+ "name": "core_users_sso",
+ "schema": "",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerId": {
+ "name": "providerId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "core_users_sso_user_id_idx": {
+ "name": "core_users_sso_user_id_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "core_users_sso_userId_core_users_id_fk": {
+ "name": "core_users_sso_userId_core_users_id_fk",
+ "tableFrom": "core_users_sso",
+ "tableTo": "core_users",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.blog_categories": {
+ "name": "blog_categories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "titleSeo": {
+ "name": "titleSeo",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "blog_categories_titleSeo_unique": {
+ "name": "blog_categories_titleSeo_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "titleSeo"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "public.blog_posts": {
+ "name": "blog_posts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "titleSeo": {
+ "name": "titleSeo",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "categoryId": {
+ "name": "categoryId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "blog_posts_categoryId_blog_categories_id_fk": {
+ "name": "blog_posts_categoryId_blog_categories_id_fk",
+ "tableFrom": "blog_posts",
+ "tableTo": "blog_categories",
+ "columnsFrom": [
+ "categoryId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "blog_posts_titleSeo_unique": {
+ "name": "blog_posts_titleSeo_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "titleSeo"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/docs/migrations/meta/_journal.json b/apps/docs/migrations/meta/_journal.json
index affe4d6d8..afc6ccdd9 100644
--- a/apps/docs/migrations/meta/_journal.json
+++ b/apps/docs/migrations/meta/_journal.json
@@ -64,6 +64,13 @@
"when": 1783283845218,
"tag": "0008_silky_forgotten_one",
"breakpoints": true
+ },
+ {
+ "idx": 9,
+ "version": "7",
+ "when": 1783880180694,
+ "tag": "0009_modern_eternals",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/apps/docs/src/examples/color.tsx b/apps/docs/src/examples/color.tsx
new file mode 100644
index 000000000..95781f7cf
--- /dev/null
+++ b/apps/docs/src/examples/color.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { AutoForm } from "@vitnode/core/components/form/auto-form";
+import { AutoFormColor } from "@vitnode/core/components/form/fields/color";
+import { z } from "zod";
+
+export default function ColorExample() {
+ const formSchema = z.object({
+ color: z.string().default("hsl(240, 80%, 60%)"),
+ });
+
+ return (
+ (
+
+ ),
+ },
+ ]}
+ formSchema={formSchema}
+ onSubmit={values => {
+ // eslint-disable-next-line no-console
+ console.log(values.color);
+ }}
+ />
+ );
+}
diff --git a/apps/docs/src/locales/@vitnode/core/en.json b/apps/docs/src/locales/@vitnode/core/en.json
index ebfd410cf..8a78cce11 100644
--- a/apps/docs/src/locales/@vitnode/core/en.json
+++ b/apps/docs/src/locales/@vitnode/core/en.json
@@ -103,6 +103,7 @@
"select_option": "Select an option",
"select_options": "Select options",
"select_language": "Select language",
+ "pick_color": "Pick a color",
"go_to_prev_page": "Go to previous page",
"go_to_next_page": "Go to next page",
"errors": {
diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json
index a88a7f1f1..548617acf 100644
--- a/packages/vitnode/package.json
+++ b/packages/vitnode/package.json
@@ -128,6 +128,7 @@
"next-themes": "^0.4.6",
"postgres": "^3.4.9",
"rate-limiter-flexible": "^11.2.0",
+ "react-colorful": "^5.6.1",
"react-day-picker": "^10.0.1",
"react-resizable-panels": "^4.11.2",
"react-scan": "^0.5.7",
diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts
index e6769ff3f..ca7b4a77c 100644
--- a/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts
+++ b/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts
@@ -18,7 +18,7 @@ export const zodRoleNameSchema = z
export const zodCreateRoleAdminSchema = z.object({
name: zodRoleNameSchema,
- color: z.string().max(19).optional(),
+ color: z.string().max(50).optional(),
});
export const createRoleAdminRoute = buildRoute({
diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts
index 4afab926d..f7293838f 100644
--- a/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts
+++ b/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts
@@ -11,7 +11,7 @@ import { zodRoleNameSchema } from "./create.route";
export const zodUpdateRoleAdminSchema = z
.object({
name: zodRoleNameSchema,
- color: z.string().max(19),
+ color: z.string().max(50),
})
.partial()
.refine(body => Object.values(body).some(value => value !== undefined), {
diff --git a/packages/vitnode/src/components/form/fields/color.test.tsx b/packages/vitnode/src/components/form/fields/color.test.tsx
new file mode 100644
index 000000000..4dbb9979b
--- /dev/null
+++ b/packages/vitnode/src/components/form/fields/color.test.tsx
@@ -0,0 +1,135 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { type FieldValues, useForm } from "react-hook-form";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { Form, FormField } from "@/components/ui/form";
+
+import { AutoFormColor } from "./color";
+
+vi.mock("next-intl", () => ({
+ useLocale: () => "en",
+ useTranslations: () => (key: string) => key,
+}));
+
+vi.mock("react-colorful", () => ({
+ HslStringColorPicker: ({
+ color,
+ onChange,
+ }: {
+ color?: string;
+ onChange?: (value: string) => void;
+ }) => (
+
+ ),
+}));
+
+const Harness = ({
+ onSubmit = vi.fn(),
+ defaultValue = "hsl(240, 80%, 60%)",
+}: {
+ defaultValue?: string;
+ onSubmit?: (values: FieldValues) => void;
+}) => {
+ const form = useForm({
+ defaultValues: { color: defaultValue } as FieldValues,
+ });
+
+ return (
+
+ );
+};
+
+describe("AutoFormColor", () => {
+ beforeEach(() => {
+ Element.prototype.scrollIntoView = vi.fn();
+ Element.prototype.hasPointerCapture = vi.fn(() => false);
+ Element.prototype.setPointerCapture = vi.fn();
+ Element.prototype.releasePointerCapture = vi.fn();
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders the label and the current color on the trigger", () => {
+ render();
+
+ expect(screen.getByText("Color")).toBeDefined();
+ expect(screen.getByText("hsl(240, 80%, 60%)")).toBeDefined();
+ });
+
+ it("shows the pick-color placeholder when no color is set", () => {
+ render();
+
+ expect(screen.getByText("pick_color")).toBeDefined();
+ });
+
+ it("writes the picked HSL color into the form and submits it", async () => {
+ const onSubmit = vi.fn();
+ render();
+
+ fireEvent.click(screen.getByText("hsl(240, 80%, 60%)"));
+ fireEvent.click(await screen.findByTestId("picker"));
+
+ fireEvent.click(screen.getByRole("button", { name: "submit" }));
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith(
+ { color: "hsl(200, 50%, 50%)" },
+ expect.anything(),
+ );
+ });
+ });
+
+ it("lets the user type a color in any format via the text input", async () => {
+ const onSubmit = vi.fn();
+ render();
+
+ fireEvent.click(screen.getByText("hsl(240, 80%, 60%)"));
+ const input = await screen.findByRole("textbox");
+ fireEvent.change(input, { target: { value: "#00ff00" } });
+
+ fireEvent.click(screen.getByRole("button", { name: "submit" }));
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith(
+ { color: "#00ff00" },
+ expect.anything(),
+ );
+ });
+ });
+
+ it("clears the value via the remove button when allowRemoveColor is set", async () => {
+ const onSubmit = vi.fn();
+ render();
+
+ fireEvent.click(screen.getByText("hsl(240, 80%, 60%)"));
+ fireEvent.click(await screen.findByRole("button", { name: "remove" }));
+
+ fireEvent.click(screen.getByRole("button", { name: "submit" }));
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith({ color: "" }, expect.anything());
+ });
+ });
+});
diff --git a/packages/vitnode/src/components/form/fields/color.tsx b/packages/vitnode/src/components/form/fields/color.tsx
new file mode 100644
index 000000000..10f5b5ffa
--- /dev/null
+++ b/packages/vitnode/src/components/form/fields/color.tsx
@@ -0,0 +1,43 @@
+import type React from "react";
+
+import { ColorPicker } from "@/components/ui/color-picker";
+import { FormControl, FormMessage } from "@/components/ui/form";
+
+import type { ItemAutoFormComponentProps } from "../auto-form";
+
+import { AutoFormDesc } from "../common/desc";
+import { AutoFormLabel } from "../common/label";
+
+export const AutoFormColor = ({
+ label,
+ labelRight,
+ description,
+ otherProps: { isOptional },
+ field,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ itemParams,
+ ...props
+}: ItemAutoFormComponentProps &
+ Omit, "onChange" | "value">) => {
+ return (
+ <>
+ {label && (
+
+ {label}
+
+ )}
+
+
+
+
+
+ {description && {description}}
+
+ >
+ );
+};
diff --git a/packages/vitnode/src/components/ui/color-picker.tsx b/packages/vitnode/src/components/ui/color-picker.tsx
new file mode 100644
index 000000000..b1869ee28
--- /dev/null
+++ b/packages/vitnode/src/components/ui/color-picker.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { XIcon } from "lucide-react";
+import { useTranslations } from "next-intl";
+import React from "react";
+
+import { cn } from "@/lib/utils";
+
+import { Button } from "./button";
+import { Input } from "./input";
+import { Loader } from "./loader";
+import { Popover, PopoverContent, PopoverTrigger } from "./popover";
+
+// react-colorful only ships in the bundle once the picker is actually opened.
+const HslStringColorPicker = React.lazy(async () => ({
+ default: (await import("react-colorful")).HslStringColorPicker,
+}));
+
+export const ColorPicker = ({
+ value = "",
+ onChange,
+ placeholder,
+ allowRemoveColor,
+ className,
+ ...props
+}: Omit, "children" | "onChange" | "value"> & {
+ allowRemoveColor?: boolean;
+ onChange?: (value: string) => void;
+ placeholder?: string;
+ value?: string;
+}) => {
+ const t = useTranslations("core.global");
+
+ return (
+
+
+ }
+ >
+
+
+ {value || (placeholder ?? t("pick_color"))}
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+ onChange?.(event.target.value)}
+ placeholder={placeholder ?? "hsl(240, 80%, 60%)"}
+ value={value}
+ />
+
+ {allowRemoveColor && value && (
+
+ )}
+
+
+ );
+};
diff --git a/packages/vitnode/src/database/roles.ts b/packages/vitnode/src/database/roles.ts
index 7b5a3601d..7c8dcb859 100644
--- a/packages/vitnode/src/database/roles.ts
+++ b/packages/vitnode/src/database/roles.ts
@@ -11,5 +11,5 @@ export const core_roles = pgTable("core_roles", t => ({
default: t.boolean().notNull().default(false),
root: t.boolean().notNull().default(false),
guest: t.boolean().notNull().default(false),
- color: t.varchar({ length: 19 }),
+ color: t.varchar({ length: 50 }),
})).enableRLS();
diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json
index ebfd410cf..8a78cce11 100644
--- a/packages/vitnode/src/locales/en.json
+++ b/packages/vitnode/src/locales/en.json
@@ -103,6 +103,7 @@
"select_option": "Select an option",
"select_options": "Select options",
"select_language": "Select language",
+ "pick_color": "Pick a color",
"go_to_prev_page": "Go to previous page",
"go_to_next_page": "Go to next page",
"errors": {
diff --git a/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.test.tsx b/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.test.tsx
index 82cf5792e..11e523043 100644
--- a/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.test.tsx
+++ b/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.test.tsx
@@ -89,10 +89,10 @@ describe("CreateEditRoleAdmin", () => {
],
});
- const [nameInput, colorInput] =
- screen.getAllByRole("textbox");
+ // The name is a text input; the color renders on the color-picker trigger.
+ const [nameInput] = screen.getAllByRole("textbox");
expect(nameInput.value).toBe("Admin");
- expect(colorInput.value).toBe("#ef4444");
+ expect(screen.getByText("#ef4444")).toBeDefined();
submitForm(nameInput);
diff --git a/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.tsx b/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.tsx
index 229a2b532..0be05d0a4 100644
--- a/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.tsx
+++ b/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.tsx
@@ -3,6 +3,7 @@ import { toast } from "sonner";
import { z } from "zod";
import { AutoForm, type AutoFormOnSubmit } from "@/components/form/auto-form";
+import { AutoFormColor } from "@/components/form/fields/color";
import { AutoFormInput } from "@/components/form/fields/input";
import { useDialog } from "@/components/ui/dialog";
import { multiLangValueSchema } from "@/lib/helpers/multi-lang";
@@ -34,7 +35,7 @@ export const CreateEditRoleAdmin = ({ data }: { data?: RoleData }) => {
),
color: z
.string()
- .max(19)
+ .max(50)
.default(data?.color ?? ""),
});
@@ -68,9 +69,9 @@ export const CreateEditRoleAdmin = ({ data }: { data?: RoleData }) => {
{
id: "color",
component: props => (
-
),
diff --git a/packages/vitnode/src/views/admin/views/core/users/roles/roles-admin-view.tsx b/packages/vitnode/src/views/admin/views/core/users/roles/roles-admin-view.tsx
index d29fe0ff9..2d7250390 100644
--- a/packages/vitnode/src/views/admin/views/core/users/roles/roles-admin-view.tsx
+++ b/packages/vitnode/src/views/admin/views/core/users/roles/roles-admin-view.tsx
@@ -66,7 +66,7 @@ export const RolesAdminView = async ({
{
accessorKey: "updatedAt",
header: t("updatedAt"),
- cell: ({ row }) => ,
+ cell: ({ row }) => ,
},
{
id: "actions",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 028a2026b..4af932786 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -530,6 +530,9 @@ importers:
rate-limiter-flexible:
specifier: ^11.2.0
version: 11.2.0
+ react-colorful:
+ specifier: ^5.6.1
+ version: 5.7.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-day-picker:
specifier: ^10.0.1
version: 10.0.1(@types/react@19.2.17)(react@19.2.7)
@@ -7875,6 +7878,12 @@ packages:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
+ react-colorful@5.7.0:
+ resolution: {integrity: sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
react-day-picker@10.0.1:
resolution: {integrity: sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==}
engines: {node: '>=18'}
@@ -16088,6 +16097,11 @@ snapshots:
iconv-lite: 0.7.2
unpipe: 1.0.0
+ react-colorful@5.7.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
react-day-picker@10.0.1(@types/react@19.2.17)(react@19.2.7):
dependencies:
'@date-fns/tz': 1.5.0