├── demo ├── src │ ├── app │ │ └── (payload) │ │ │ ├── custom.scss │ │ │ ├── admin │ │ │ ├── importMap.js │ │ │ └── [[...segments]] │ │ │ │ ├── page.tsx │ │ │ │ └── not-found.tsx │ │ │ ├── api │ │ │ ├── graphql-playground │ │ │ │ └── route.ts │ │ │ ├── graphql │ │ │ │ └── route.ts │ │ │ └── [...slug] │ │ │ │ └── route.ts │ │ │ └── layout.tsx │ ├── collections │ │ ├── Users.ts │ │ ├── Media.ts │ │ ├── Posts.ts │ │ └── Pages.ts │ ├── globals │ │ └── Settings.ts │ ├── seed │ │ └── index.ts │ └── payload.config.ts ├── .env.example ├── next.config.mjs ├── .gitignore ├── tsconfig.json └── package.json ├── src ├── components │ ├── index.ts │ └── ColorPicker │ │ ├── types.ts │ │ ├── index.scss │ │ ├── index.tsx │ │ └── Input.tsx ├── utils │ ├── mergeFieldStyles.ts │ └── isFIeldRTL.ts └── index.ts ├── pnpm-workspace.yaml ├── .eslintrc.js ├── .gitignore ├── screenshots └── payload-color-picker-field-screenshot.png ├── .prettierrc.js ├── .editorconfig ├── .devcontainer ├── devcontainer.json ├── Dockerfile └── docker-compose.yml ├── tsconfig.json ├── LICENSE ├── eslint.config.js ├── README.md ├── .github └── workflows │ └── npm-publish.yml └── package.json /demo/src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ColorPicker'; -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '.' 3 | - 'demo' -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@payloadcms'], 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | dist 4 | demo/uploads 5 | build 6 | .DS_Store 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /demo/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URI=mongodb://localhost/payload-payload-plugin-boilerplate 2 | PAYLOAD_SECRET=SECRET_KEY_HERE 3 | -------------------------------------------------------------------------------- /demo/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | 3 | const nextConfig = {} 4 | 5 | export default withPayload(nextConfig) 6 | -------------------------------------------------------------------------------- /screenshots/payload-color-picker-field-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/innovixx/payload-color-picker-field/HEAD/screenshots/payload-color-picker-field-screenshot.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | parser: "typescript", 4 | semi: false, 5 | singleQuote: true, 6 | trailingComma: "all", 7 | arrowParens: "avoid", 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = null 11 | -------------------------------------------------------------------------------- /demo/src/collections/Users.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | export const Users: CollectionConfig = { 4 | slug: 'users', 5 | access: { 6 | read: () => true, 7 | }, 8 | auth: true, 9 | fields: [ 10 | // Email added by default 11 | // Add more fields as needed 12 | ], 13 | } -------------------------------------------------------------------------------- /demo/src/app/(payload)/admin/importMap.js: -------------------------------------------------------------------------------- 1 | import { ColorPickerFieldComponent as ColorPickerFieldComponent_95f860801d0381344387ae1d8e2f4d24 } from '@innovixx/payload-color-picker-field/components' 2 | 3 | export const importMap = { 4 | "@innovixx/payload-color-picker-field/components#ColorPickerFieldComponent": ColorPickerFieldComponent_95f860801d0381344387ae1d8e2f4d24 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 6 | 7 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 8 | -------------------------------------------------------------------------------- /demo/src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | 8 | export const OPTIONS = REST_OPTIONS(config) 9 | -------------------------------------------------------------------------------- /demo/src/globals/Settings.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalConfig } from 'payload' 2 | 3 | const Settings: GlobalConfig = { 4 | slug: 'settings', 5 | fields: [ 6 | { 7 | name: 'title', 8 | type: 'text', 9 | required: true, 10 | }, 11 | { 12 | name: 'excerpt', 13 | type: 'text', 14 | }, 15 | ], 16 | } 17 | 18 | export default Settings 19 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-color-picker-field", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "app", 5 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 6 | "customizations": { 7 | "vscode": { 8 | "extensions": [ 9 | "dbaeumer.vscode-eslint" 10 | ] 11 | } 12 | }, 13 | "forwardPorts": [ 14 | 9000, 15 | 27017 16 | ] 17 | } -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:jammy 2 | 3 | ARG USERNAME=vscode 4 | 5 | RUN su ${USERNAME} -c "bash -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash'" 6 | RUN su ${USERNAME} -c "bash -c 'source /home/${USERNAME}/.nvm/nvm.sh && nvm install 20 && nvm use 20'" 7 | RUN su ${USERNAME} -c "bash -c 'source /home/${USERNAME}/.nvm/nvm.sh && npm install -g pnpm'" -------------------------------------------------------------------------------- /demo/src/collections/Media.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | import path from 'path' 4 | 5 | export const Media: CollectionConfig = { 6 | slug: 'media', 7 | access: { 8 | read: (): boolean => true, 9 | }, 10 | fields: [ 11 | { 12 | name: 'alt', 13 | type: 'text', 14 | label: 'Alt Text', 15 | required: true, 16 | }, 17 | ], 18 | upload: { 19 | staticDir: path.join(process.cwd(), 'storage', 'media'), 20 | }, 21 | } -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - ../..:/workspaces:cached 10 | - ~/.ssh:/home/vscode/.ssh:cached 11 | - ~/.npmrc:/home/vscode/.npmrc:cached 12 | command: sleep infinity 13 | working_dir: /workspaces/payload-color-picker-field 14 | 15 | db: 16 | image: mongo:latest 17 | restart: unless-stopped 18 | volumes: 19 | - mongodb-data:/data/db 20 | network_mode: service:app 21 | 22 | volumes: 23 | mongodb-data: -------------------------------------------------------------------------------- /src/utils/mergeFieldStyles.ts: -------------------------------------------------------------------------------- 1 | import type { ClientField } from 'payload' 2 | 3 | export const mergeFieldStyles = ( 4 | field: ClientField | Omit, 5 | ): React.CSSProperties => ({ 6 | ...(field?.admin?.style || {}), 7 | ...(field?.admin?.width 8 | ? { 9 | '--field-width': field.admin.width, 10 | } 11 | : { 12 | flex: '1 1 auto', 13 | }), 14 | // allow flex overrides to still take precedence over the fallback 15 | ...(field?.admin?.style?.flex 16 | ? { 17 | flex: field.admin.style.flex, 18 | } 19 | : {}), 20 | }) 21 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | /.idea/* 10 | !/.idea/runConfigurations 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | .env 42 | 43 | /storage/* 44 | -------------------------------------------------------------------------------- /demo/src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { 6 | REST_DELETE, 7 | REST_GET, 8 | REST_OPTIONS, 9 | REST_PATCH, 10 | REST_POST, 11 | REST_PUT, 12 | } from '@payloadcms/next/routes' 13 | 14 | export const GET = REST_GET(config) 15 | export const POST = REST_POST(config) 16 | export const DELETE = REST_DELETE(config) 17 | export const PATCH = REST_PATCH(config) 18 | export const PUT = REST_PUT(config) 19 | export const OPTIONS = REST_OPTIONS(config) 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "DOM", 5 | "DOM.Iterable", 6 | "ES2022" 7 | ], 8 | "outDir": "./dist", 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "isolatedModules": true, 16 | "incremental": true, 17 | "declaration": true, 18 | "declarationDir": "./dist", 19 | "jsx": "preserve", 20 | "target": "ES2022", 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | }, 27 | "include": [ 28 | "src/**/*" 29 | ], 30 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Field, TextField } from "payload" 2 | 3 | export const colorPickerField = ( 4 | options?: { 5 | colors?: string[] 6 | } & Partial, 7 | ): Field => { 8 | const { colors, ...rest } = options || {} 9 | 10 | return { 11 | ...rest, 12 | name: rest?.name || 'colorPicker', 13 | type: 'text', 14 | admin: { 15 | ...rest?.admin, 16 | components: { 17 | ...rest?.admin?.components, 18 | Field: { 19 | clientProps: { 20 | colors 21 | }, 22 | path: '@innovixx/payload-color-picker-field/components#ColorPickerFieldComponent', 23 | }, 24 | }, 25 | }, 26 | label: rest?.label || 'Color Picker', 27 | } as TextField 28 | } -------------------------------------------------------------------------------- /demo/src/collections/Posts.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | export const Posts: CollectionConfig = { 4 | slug: 'posts', 5 | fields: [ 6 | { 7 | type: 'tabs', 8 | tabs: [ 9 | { 10 | fields: [ 11 | { 12 | name: 'title', 13 | type: 'text', 14 | required: true, 15 | }, 16 | { 17 | name: 'excerpt', 18 | type: 'text', 19 | }, 20 | ], 21 | label: 'Content', 22 | }, 23 | ], 24 | }, 25 | { 26 | name: 'slug', 27 | type: 'text', 28 | admin: { 29 | position: 'sidebar', 30 | }, 31 | label: 'Slug', 32 | required: true, 33 | }, 34 | ], 35 | versions: true, 36 | } -------------------------------------------------------------------------------- /demo/src/seed/index.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionSlug, Payload } from 'payload' 2 | 3 | export const seed = async (payload: Payload): Promise => { 4 | payload.logger.info('Seeding data...') 5 | 6 | await payload.create({ 7 | collection: 'users' as CollectionSlug, 8 | data: { 9 | email: 'admin@innovixx.co.uk', 10 | password: 'Pa$$w0rd!', 11 | }, 12 | }) 13 | 14 | await payload.create({ 15 | collection: 'pages' as CollectionSlug, 16 | data: { 17 | title: 'Home Page', 18 | slug: 'home', 19 | excerpt: 'This is the home page', 20 | }, 21 | }) 22 | 23 | await payload.create({ 24 | collection: 'posts' as CollectionSlug, 25 | data: { 26 | title: 'Hello, world!', 27 | slug: 'hello-world', 28 | excerpt: 'This is a post', 29 | }, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, RootPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const Page = ({ params, searchParams }: Args) => 23 | RootPage({ config, importMap, params, searchParams }) 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /demo/src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views' 7 | import { importMap } from '../importMap' 8 | 9 | type Args = { 10 | params: Promise<{ 11 | segments: string[] 12 | }> 13 | searchParams: Promise<{ 14 | [key: string]: string | string[] 15 | }> 16 | } 17 | 18 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 19 | generatePageMetadata({ config, params, searchParams }) 20 | 21 | const NotFound = ({ params, searchParams }: Args) => 22 | NotFoundPage({ config, params, searchParams, importMap }) 23 | 24 | export default NotFound 25 | -------------------------------------------------------------------------------- /demo/src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config'; 4 | import '@payloadcms/next/css'; 5 | import type { ServerFunctionClient } from 'payload'; 6 | import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'; 7 | import React from 'react'; 8 | 9 | import { importMap } from './admin/importMap'; 10 | import './custom.scss'; 11 | 12 | type Args = { 13 | children: React.ReactNode 14 | } 15 | 16 | const serverFunction: ServerFunctionClient = async function (args) { 17 | 'use server'; 18 | 19 | return handleServerFunctions({ 20 | ...args, 21 | config, 22 | importMap, 23 | }); 24 | }; 25 | 26 | const Layout = ({ children }: Args) => ( 27 | 28 | {children} 29 | 30 | ); 31 | 32 | export default Layout; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Innovixx Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": [ 5 | "DOM", 6 | "DOM.Iterable", 7 | "ES2022" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ], 29 | "@payload-config": [ 30 | "./src/payload.config.ts" 31 | ], 32 | "@innovixx/payload-color-picker-field": [ 33 | "../dist" 34 | ], 35 | "@innovixx/payload-color-picker-field/components": [ 36 | "../dist/components" 37 | ], 38 | }, 39 | "target": "ES2022" 40 | }, 41 | "include": [ 42 | "next-env.d.ts", 43 | "**/*.ts", 44 | "**/*.tsx", 45 | ".next/types/**/*.ts" 46 | ], 47 | "exclude": [ 48 | "node_modules" 49 | ] 50 | } -------------------------------------------------------------------------------- /demo/src/collections/Pages.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | import { colorPickerField } from '@innovixx/payload-color-picker-field' 4 | 5 | export const Pages: CollectionConfig = { 6 | slug: 'pages', 7 | fields: [ 8 | { 9 | name: 'title', 10 | type: 'text', 11 | required: true, 12 | }, 13 | { 14 | name: 'slug', 15 | type: 'text', 16 | admin: { 17 | position: 'sidebar', 18 | }, 19 | required: true, 20 | }, 21 | colorPickerField({ 22 | name: 'primaryColor', 23 | admin: { 24 | description: 'Pick a color for this page', 25 | }, 26 | colors: ['#ff0000', '#00ff00', '#0000ff', '#000000', '#ffffff', '#ff00ff'], 27 | label: 'Primary Color', 28 | }), 29 | { 30 | name: 'excerpt', 31 | type: 'text', 32 | }, 33 | { 34 | name: 'date', 35 | type: 'date', 36 | }, 37 | // colorPickerField({ 38 | // name: 'secondaryColor', 39 | // label: 'Secondary Color', 40 | 41 | // admin: { 42 | // description: 'Pick a secondary color for this page', 43 | // position: 'sidebar', 44 | // rtl: true, 45 | // }, 46 | // }), 47 | ], 48 | } 49 | -------------------------------------------------------------------------------- /demo/src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { mongooseAdapter } from '@payloadcms/db-mongodb' 2 | import { lexicalEditor } from '@payloadcms/richtext-lexical' 3 | import path from 'path' 4 | import { buildConfig } from 'payload' 5 | import { fileURLToPath } from 'url' 6 | 7 | import { Media } from './collections/Media' 8 | import { Pages } from './collections/Pages' 9 | import { Posts } from './collections/Posts' 10 | import { Users } from './collections/Users' 11 | import { seed } from './seed' 12 | 13 | const filename = fileURLToPath(import.meta.url) 14 | const dirname = path.dirname(filename) 15 | // eslint-disable-next-line no-restricted-exports 16 | export default buildConfig({ 17 | admin: { 18 | importMap: { 19 | baseDir: path.resolve(dirname), 20 | }, 21 | user: Users.slug, 22 | }, 23 | collections: [Media, Pages, Users, Posts], 24 | db: mongooseAdapter({ 25 | url: process.env.DATABASE_URI || '', 26 | }), 27 | editor: lexicalEditor({}), 28 | graphQL: { 29 | schemaOutputFile: path.resolve(dirname, 'lib/schema.graphql'), 30 | }, 31 | onInit: async payload => { 32 | if (process.env.NODE_ENV === 'development' && process.env.PAYLOAD_SEED_DATABASE) { 33 | await seed(payload) 34 | } 35 | }, 36 | secret: process.env.PAYLOAD_SECRET || '', 37 | typescript: { 38 | outputFile: path.resolve(dirname, 'lib/types.ts'), 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/utils/isFIeldRTL.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import type { Locale, SanitizedLocalizationConfig } from 'payload' 3 | 4 | export const fieldBaseClass = 'field-type' 5 | 6 | /** 7 | * Determines whether a field should be displayed as right-to-left (RTL) based on its configuration, payload's localization configuration and the adming user's currently enabled locale. 8 | 9 | * @returns Whether the field should be displayed as RTL. 10 | */ 11 | export function isFieldRTL({ 12 | fieldLocalized, 13 | fieldRTL, 14 | locale, 15 | localizationConfig, 16 | }: { 17 | fieldLocalized: boolean 18 | fieldRTL: boolean 19 | locale: Locale 20 | localizationConfig?: SanitizedLocalizationConfig 21 | }) { 22 | const hasMultipleLocales = 23 | locale && 24 | localizationConfig && 25 | localizationConfig.locales && 26 | localizationConfig.locales.length > 1 27 | 28 | const isCurrentLocaleDefaultLocale = locale?.code === localizationConfig?.defaultLocale 29 | 30 | return ( 31 | (fieldRTL !== false && 32 | locale?.rtl === true && 33 | (fieldLocalized || 34 | (!fieldLocalized && !hasMultipleLocales) || // If there is only one locale which is also rtl, that field is rtl too 35 | (!fieldLocalized && isCurrentLocaleDefaultLocale))) || // If the current locale is the default locale, but the field is not localized, that field is rtl too 36 | fieldRTL === true 37 | ) // If fieldRTL is true. This should be useful for when no localization is set at all in the payload config, but you still want fields to be rtl. 38 | } 39 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import payloadEsLintConfig from '@payloadcms/eslint-config' 2 | import payloadPlugin from '@payloadcms/eslint-plugin' 3 | 4 | export const defaultESLintIgnores = [ 5 | '**/.temp', 6 | '**/.*', 7 | '**/.git', 8 | '**/tsconfig.tsbuildinfo', 9 | '**/README.md', 10 | '**/eslint.config.js', 11 | '**/dist/', 12 | '**/node_modules/', 13 | ] 14 | 15 | /** @typedef {import('eslint').Linter.Config} Config */ 16 | 17 | export const rootParserOptions = { 18 | sourceType: 'module', 19 | ecmaVersion: 'latest', 20 | projectService: { 21 | maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 40, 22 | allowDefaultProject: ['scripts/*.ts', '*.js', '*.mjs', '*.d.ts'], 23 | }, 24 | } 25 | 26 | /** @type {Config[]} */ 27 | export const rootEslintConfig = [ 28 | ...payloadEsLintConfig, 29 | { 30 | ignores: [ 31 | ...defaultESLintIgnores, 32 | ], 33 | }, 34 | { 35 | plugins: { 36 | payload: payloadPlugin, 37 | }, 38 | rules: { 39 | 'payload/no-jsx-import-statements': 'warn', 40 | 'payload/no-relative-monorepo-imports': 'error', 41 | 'payload/no-imports-from-exports-dir': 'error', 42 | 'payload/no-imports-from-self': 'error', 43 | 'payload/proper-payload-logger-usage': 'error', 44 | }, 45 | }, 46 | ] 47 | 48 | export default [ 49 | ...rootEslintConfig, 50 | { 51 | languageOptions: { 52 | parserOptions: { 53 | ...rootParserOptions, 54 | projectService: true, 55 | tsconfigRootDir: import.meta.dirname, 56 | }, 57 | }, 58 | }, 59 | 60 | ] -------------------------------------------------------------------------------- /src/components/ColorPicker/types.ts: -------------------------------------------------------------------------------- 1 | import type { FieldClientComponent, StaticDescription, StaticLabel, TextFieldClient, TextFieldValidation } from 'payload' 2 | import type { ChangeEvent } from 'react' 3 | import type React from 'react' 4 | import type { MarkOptional } from 'ts-essentials'; 5 | 6 | export type SharedColorPickerFieldProps = 7 | | { 8 | readonly hasMany?: false 9 | readonly onChange?: (e: ChangeEvent) => void 10 | } 11 | 12 | export type ColorPickerInputProps = { 13 | readonly AfterInput?: React.ReactNode 14 | readonly BeforeInput?: React.ReactNode 15 | readonly className?: string 16 | readonly colors?: string[] 17 | readonly Description?: React.ReactNode 18 | readonly description?: StaticDescription 19 | readonly Error?: React.ReactNode 20 | readonly inputRef?: React.RefObject 21 | readonly Label?: React.ReactNode 22 | readonly label?: StaticLabel 23 | readonly localized?: boolean 24 | readonly onKeyDown?: React.KeyboardEventHandler 25 | readonly path: string 26 | readonly placeholder?: Record | string 27 | readonly readOnly?: boolean 28 | readonly required?: boolean 29 | readonly rtl?: boolean 30 | readonly showError?: boolean 31 | readonly style?: React.CSSProperties 32 | readonly value?: string 33 | } & SharedColorPickerFieldProps 34 | 35 | type ColorPickerFieldClientWithoutType = MarkOptional; 36 | type ColorPickerFieldBaseClientProps = { 37 | readonly colors?: string[]; 38 | readonly inputRef?: React.RefObject; 39 | readonly onKeyDown?: React.KeyboardEventHandler; 40 | readonly path: string; 41 | readonly validate?: TextFieldValidation; 42 | }; 43 | export type ColorPickerFieldClientComponent = FieldClientComponent; -------------------------------------------------------------------------------- /src/components/ColorPicker/index.scss: -------------------------------------------------------------------------------- 1 | @import '@payloadcms/ui/scss'; 2 | 3 | .field-type.color { 4 | position: relative; 5 | 6 | input { 7 | @include formInput; 8 | } 9 | } 10 | 11 | .color { 12 | &__input-container { 13 | position: relative; 14 | display: flex; 15 | } 16 | 17 | &__color-preview { 18 | height: base(2); 19 | width: base(2); 20 | border: 1px solid var(--theme-elevation-150); 21 | } 22 | 23 | &__color-picker-modal { 24 | position: absolute; 25 | top: 100%; 26 | left: 0; 27 | 28 | display: flex; 29 | flex-direction: row; 30 | 31 | margin-top: base(0.5); 32 | z-index: $z-modal; 33 | 34 | visibility: hidden; 35 | opacity: 0; 36 | 37 | transition: all 0.1s ease-in-out; 38 | 39 | &__predefined-colors { 40 | display: flex; 41 | flex-direction: column; 42 | 43 | & * { 44 | width: base(1); 45 | flex: 1; 46 | 47 | &:focus, 48 | &:hover { 49 | opacity: 1; 50 | } 51 | } 52 | } 53 | 54 | &__button__color { 55 | display: block; 56 | width: 100%; 57 | height: 100%; 58 | } 59 | 60 | & .react-colorful__saturation, 61 | .react-colorful__hue { 62 | border-radius: 0; 63 | } 64 | 65 | & .react-colorful__pointer { 66 | border-radius: 0; 67 | width: base(0.95); 68 | height: base(0.95); 69 | } 70 | } 71 | 72 | &__color-picker-modal--rtl { 73 | left: auto; 74 | right: 0; 75 | } 76 | 77 | &__color-picker-modal--focused { 78 | visibility: visible; 79 | opacity: 1; 80 | } 81 | } 82 | 83 | html[data-theme='light'] { 84 | .field-type.color { 85 | &.error { 86 | input { 87 | @include lightInputError; 88 | } 89 | } 90 | } 91 | } 92 | 93 | html[data-theme='dark'] { 94 | .field-type.color { 95 | &.error { 96 | input { 97 | @include darkInputError; 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website-payload-boilerplate", 3 | "version": "0.0.0", 4 | "description": "A boilerplate for a Payload server", 5 | "repository": "git@github.com:innovixx/website-payload-boilerplate.git", 6 | "author": "Innovixx ", 7 | "license": "MIT", 8 | "private": true, 9 | "type": "module", 10 | "scripts": { 11 | "dev": "next dev -p 9000", 12 | "cleanDev": "rm -rf ./storage && cross-env PAYLOAD_DROP_DATABASE=true PAYLOAD_SEED_DATABASE=true next dev -p 9000", 13 | "build": "next build", 14 | "serve": "cross-env NODE_ENV=production next start -p 9000", 15 | "payload": "payload", 16 | "generate:types": "payload generate:types", 17 | "generate:graphql": "payload generate:graphQLSchema", 18 | "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 19 | "lint-staged": "lint-staged --verbose", 20 | "prepare": "husky" 21 | }, 22 | "dependencies": { 23 | "@azure/storage-blob": "^12.26.0", 24 | "@payloadcms/db-mongodb": "3.16.0", 25 | "@payloadcms/next": "3.16.0", 26 | "@payloadcms/richtext-lexical": "3.16.0", 27 | "@payloadcms/ui": "^3.16.0", 28 | "body-parser": "^1.20.2", 29 | "cross-env": "^7.0.3", 30 | "dotenv": "^16.4.5", 31 | "express": "^4.19.2", 32 | "graphql": "^16.9.0", 33 | "next": "15.1.9", 34 | "payload": "3.16.0", 35 | "react": "19.0.0", 36 | "react-colorful": "^5.6.1", 37 | "react-dom": "19.0.0", 38 | "sharp": "^0.33.5" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^20.14.14", 42 | "@types/react": "19.0.0", 43 | "@types/react-dom": "19.0.0", 44 | "@typescript-eslint/eslint-plugin": "^8.3.0", 45 | "@typescript-eslint/parser": "^8.3.0", 46 | "copyfiles": "^2.4.1", 47 | "eslint": "^8.57.0", 48 | "eslint-plugin-import": "^2.29.1", 49 | "eslint-plugin-node": "^11.1.0", 50 | "eslint-plugin-sort-export-all": "^1.4.1", 51 | "eslint-plugin-sort-keys-fix": "^1.1.2", 52 | "husky": "^9.1.5", 53 | "lint-staged": "^14.0.1", 54 | "nodemon": "^3.1.4", 55 | "ts-node": "^10.9.2", 56 | "typescript": "^5.5.4" 57 | }, 58 | "lint-staged": { 59 | "**/*.{js,ts,jsx,tsx}": [ 60 | "eslint", 61 | "bash -c tsc" 62 | ] 63 | }, 64 | "engines": { 65 | "node": "^18.20.2 || >=20.9.0" 66 | } 67 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Payload Color Picker Field 2 | 3 | [![NPM](https://img.shields.io/npm/v/@innovixx/payload-color-picker-field)](https://www.npmjs.com/package/@innovixx/payload-color-picker-field) 4 | 5 | A field for [Payload](https://github.com/payloadcms/payload) that enables an easy color selection field for your Payload projects. 6 | 7 | ![payload-color-picker-field-screenshot.png](https://github.com/Innovixx-Development/payload-color-picker-field/blob/master/screenshots/payload-color-picker-field-screenshot.png?raw=true) 8 | 9 | ## Core features: 10 | 11 | - Add a color picker field to your Payload collections 12 | - Supports HEX color formats 13 | 14 | ## Installation 15 | 16 | ```bash 17 | yarn add @innovixx/payload-color-picker-field 18 | # OR 19 | npm i @innovixx/payload-color-picker-field 20 | ``` 21 | 22 | ## Basic Usage 23 | 24 | ```js 25 | import type { CollectionConfig } from 'payload/types' 26 | 27 | import { colorPickerField } from '@innovixx/payload-color-picker-field' 28 | 29 | const Pages: CollectionConfig = { 30 | slug: 'pages', 31 | admin: { 32 | useAsTitle: 'title', 33 | }, 34 | fields: [ 35 | colorPickerField({ 36 | name: 'primaryColor', 37 | label: 'Primary Color', 38 | required: true, 39 | admin: { 40 | position: 'sidebar', 41 | description: 'Choose a color for this page', 42 | }, 43 | }), 44 | ], 45 | } 46 | 47 | export default Pages 48 | ``` 49 | 50 | ## Development 51 | 52 | To actively develop or debug this plugin you can either work directly within the demo directory of this repo, or link your own project. 53 | 54 | 1. #### Internal Demo 55 | 56 | This repo includes a fully working, self-seeding instance of Payload that installs the plugin directly from the source code. This is the easiest way to get started. To spin up this demo, follow these steps: 57 | 58 | 1. First clone the repo 59 | 1. Then, `cd YOUR_PLUGIN_REPO && yarn && cd demo && yarn && yarn cleanDev` 60 | 1. Now open `http://localhost:3000/admin` in your browser 61 | 1. Enter username `admin@innovixx.co.uk` and password `Pa$$w0rd!` 62 | 63 | That's it! Changes made in `./src` will be reflected in your demo. Keep in mind that the demo database is automatically seeded on every startup, any changes you make to the data get destroyed each time you reboot the app. -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | environment: production 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 20 16 | 17 | - name: Install pnpm 18 | run: npm install -g pnpm 19 | 20 | - name: Install dependencies 21 | run: pnpm install 22 | 23 | - name: Build 24 | run: pnpm build 25 | 26 | - name: Remove devDependencies 27 | run: pnpm prune --prod 28 | 29 | - name: Zip artifact 30 | run: zip -r artifact.zip package.json dist node_modules README.md LICENSE -q 31 | 32 | - name: Upload artifact 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: artifact 36 | path: ./artifact.zip 37 | 38 | publish-npm: 39 | needs: build 40 | runs-on: ubuntu-latest 41 | environment: production 42 | steps: 43 | - uses: actions/download-artifact@v4 44 | with: 45 | name: artifact 46 | path: . 47 | 48 | - name: Unzip artifact 49 | run: | 50 | unzip -q artifact.zip 51 | rm artifact.zip 52 | 53 | - uses: actions/setup-node@v3 54 | with: 55 | node-version: 20 56 | registry-url: https://registry.npmjs.org/ 57 | 58 | - name: Publish to NPM 59 | run: | 60 | if [ "${{ github.event.release.prerelease }}" = "true" ]; then 61 | npm publish --tag beta 62 | else 63 | npm publish 64 | fi 65 | env: 66 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 67 | 68 | # publish-github: 69 | # needs: build 70 | # runs-on: ubuntu-latest 71 | # environment: production 72 | # permissions: 73 | # packages: write 74 | # contents: read 75 | # steps: 76 | # - uses: actions/download-artifact@v2 77 | # with: 78 | # name: artifact 79 | # path: . 80 | 81 | # - name: Unzip artifact 82 | # run: | 83 | # unzip -q artifact.zip 84 | # rm artifact.zip 85 | 86 | # - uses: actions/setup-node@v3 87 | # with: 88 | # node-version: 18 89 | # registry-url: https://npm.pkg.github.com/ 90 | 91 | # - name: Publish to GitHub Packages 92 | # run: npm publish 93 | # env: 94 | # NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /src/components/ColorPicker/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useConfig, useField, useLocale, withCondition } from '@payloadcms/ui' 4 | import React, { useCallback, useMemo } from 'react' 5 | 6 | import type { ColorPickerFieldClientComponent } from './types.js' 7 | 8 | import './index.scss' 9 | import { isFieldRTL } from '../../utils/isFIeldRTL.js' 10 | import { mergeFieldStyles } from '../../utils/mergeFieldStyles.js' 11 | 12 | // eslint-disable-next-line payload/no-jsx-import-statements 13 | import { ColorPickerInput } from './Input.jsx' 14 | 15 | const ColorPickerField: ColorPickerFieldClientComponent = (props) => { 16 | const { 17 | colors, 18 | field, 19 | field: { 20 | admin: { className, description, placeholder, rtl } = {}, 21 | label, 22 | localized, 23 | maxLength, 24 | minLength, 25 | required, 26 | }, 27 | inputRef, 28 | path, 29 | readOnly, 30 | validate, 31 | } = props 32 | 33 | const locale = useLocale() 34 | 35 | const { 36 | config: { 37 | localization 38 | } 39 | } = useConfig() 40 | 41 | const memoizedValidate = useCallback( 42 | (value, options) => { 43 | if (typeof validate === 'function') { 44 | return validate(value, { ...options, maxLength, minLength, required }) 45 | } 46 | }, 47 | [validate, minLength, maxLength, required], 48 | ) 49 | 50 | const { 51 | customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, 52 | setValue, 53 | showError, 54 | value, 55 | } = useField({ 56 | path, 57 | validate: memoizedValidate, 58 | }) 59 | 60 | const renderRTL = isFieldRTL({ 61 | fieldLocalized: localized, 62 | fieldRTL: rtl, 63 | locale, 64 | localizationConfig: localization || undefined, 65 | }) 66 | 67 | const styles = useMemo(() => mergeFieldStyles(field), [field]) 68 | 69 | return ( 70 | { 83 | if (e.target.value !== value) { 84 | setValue(e.target.value); 85 | } 86 | }} 87 | path={path} 88 | placeholder={placeholder} 89 | readOnly={readOnly} 90 | required={required} 91 | rtl={renderRTL} 92 | showError={showError} 93 | style={styles} 94 | value={(value as string) || ''} 95 | /> 96 | ) 97 | } 98 | 99 | export const ColorPickerFieldComponent = withCondition(ColorPickerField) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@innovixx/payload-color-picker-field", 3 | "version": "2.0.4", 4 | "homepage:": "https://innovixx.co.uk", 5 | "repository": "git@github.com:Innovixx-Development/payload-color-picker-field.git", 6 | "description": "The Payload Color Picker that enables an easy color selection field for your Payload projects.", 7 | "type": "module", 8 | "scripts": { 9 | "build": "tsc && pnpm copy:scss", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "lint": "eslint src", 12 | "lint:fix": "eslint --fix --ext .ts,.tsx src", 13 | "copy:scss": "copyfiles -u 1 \"src/**/*.scss\" dist", 14 | "watch": "nodemon --watch src -e ts,tsx,scss --exec \"pnpm build\"" 15 | }, 16 | "keywords": [ 17 | "payload", 18 | "cms", 19 | "field", 20 | "plugin", 21 | "typescript", 22 | "react", 23 | "color", 24 | "picker" 25 | ], 26 | "author": "support@innovixx.co.uk", 27 | "license": "MIT", 28 | "peerDependencies": { 29 | "next": "15.1.9", 30 | "payload": "^3.2.2", 31 | "react": "^19.0.0" 32 | }, 33 | "devDependencies": { 34 | "@payloadcms/eslint-config": "^3.0.0", 35 | "@payloadcms/eslint-plugin": "^3.0.0", 36 | "@payloadcms/next": "3.16.0", 37 | "@payloadcms/translations": "3.16.0", 38 | "@payloadcms/ui": "3.16.0", 39 | "@types/node": "^20.14.14", 40 | "@types/react": "npm:types-react@19.0.0-rc.1", 41 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", 42 | "@typescript-eslint/eslint-plugin": "5.12.1", 43 | "@typescript-eslint/parser": "5.12.1", 44 | "copyfiles": "^2.4.1", 45 | "cross-env": "^7.0.3", 46 | "next": "15.1.9", 47 | "nodemon": "^2.0.6", 48 | "payload": "3.16.0", 49 | "prettier": "^2.7.1", 50 | "react": "19.0.0", 51 | "react-dom": "19.0.0", 52 | "ts-essentials": "^10.0.3", 53 | "ts-node": "^9.1.1", 54 | "typescript": "5.7.2" 55 | }, 56 | "files": [ 57 | "dist" 58 | ], 59 | "main": "./dist/index.js", 60 | "types": "./dist/index.d.ts", 61 | "exports": { 62 | ".": { 63 | "import": "./dist/index.js", 64 | "types": "./dist/index.d.ts", 65 | "default": "./dist/index.js" 66 | }, 67 | "./components": { 68 | "import": "./dist/components/index.js", 69 | "types": "./dist/components/index.d.ts", 70 | "default": "./dist/components/index.js" 71 | } 72 | }, 73 | "dependencies": { 74 | "react-colorful": "^5.6.1" 75 | }, 76 | "pnpm": { 77 | "overrides": { 78 | "@types/react": "npm:types-react@19.0.0-rc.1", 79 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" 80 | }, 81 | "onlyBuiltDependencies": [ 82 | "esbuild", 83 | "sharp" 84 | ] 85 | }, 86 | "overrides": { 87 | "@types/react": "npm:types-react@19.0.0-rc.1", 88 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" 89 | } 90 | } -------------------------------------------------------------------------------- /src/components/ColorPicker/Input.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import type { ChangeEvent } from 'react' 3 | 4 | import { getTranslation } from '@payloadcms/translations' 5 | import { Button, fieldBaseClass, FieldDescription, FieldError, FieldLabel, RenderCustomComponent, useTranslation } from '@payloadcms/ui' 6 | import React, { useState, useRef } from 'react' 7 | import { HexColorPicker } from 'react-colorful' 8 | 9 | import type { ColorPickerInputProps } from './types.js' 10 | 11 | import './index.scss' 12 | 13 | const baseClass = 'color' 14 | 15 | export const ColorPickerInput: React.FC = (props) => { 16 | const { 17 | AfterInput, 18 | BeforeInput, 19 | className, 20 | colors, 21 | Description, 22 | description, 23 | Error, 24 | inputRef, 25 | Label, 26 | label, 27 | localized, 28 | onChange, 29 | onKeyDown, 30 | path, 31 | placeholder, 32 | readOnly, 33 | required, 34 | rtl, 35 | showError, 36 | style, 37 | value, 38 | } = props 39 | 40 | const [fieldIsFocused, setFieldIsFocused] = useState(false) 41 | const lastValueRef = useRef(value) 42 | 43 | const { i18n, t } = useTranslation() 44 | 45 | const handleChange = (evt: ChangeEvent) => { 46 | if (!evt.target.value.startsWith('#')) { 47 | evt.target.value = `#${evt.target.value}` 48 | } 49 | 50 | evt.target.value = evt.target.value.replace(/[^a-f0-9#]/gi, '').slice(0, 7) 51 | 52 | if (lastValueRef.current !== evt.target.value) { 53 | lastValueRef.current = evt.target.value 54 | onChange?.(evt as any) 55 | } 56 | } 57 | 58 | return ( 59 |
71 | 75 | } 76 | /> 77 |
78 | } 81 | /> 82 | {BeforeInput} 83 |
{ 86 | if (!e.currentTarget.contains(e.relatedTarget)) { 87 | setFieldIsFocused(false) 88 | } 89 | }} 90 | onFocus={() => setFieldIsFocused(true)} 91 | > 92 | {!rtl && ( 93 |
1 ? value : '#fff', 97 | }} 98 | /> 99 | )} 100 | 112 | {rtl && ( 113 |
1 ? value : '#fff', 117 | }} 118 | /> 119 | )} 120 |
127 | {colors && ( 128 |
129 | {colors.map((color, index) => ( 130 | 152 | ))} 153 |
154 | )} 155 | { 158 | if (v !== value) { 159 | onChange?.({ 160 | target: { 161 | name: path, 162 | value: v, 163 | }, 164 | } as any) 165 | } 166 | }} 167 | /> 168 |
169 |
170 | {AfterInput} 171 | } 174 | /> 175 |
176 |
177 | ) 178 | } --------------------------------------------------------------------------------