@nan0web/ui-payload 3.3.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,19 +1,55 @@
1
1
  {
2
2
  "name": "@nan0web/ui-payload",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "NaN0Web Universal Payload CMS UI Components Package (ImageCell, MapCell, BooleanCell, richtext)",
5
5
  "type": "module",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
6
+ "main": "./src/index.js",
7
+ "types": "./types/index.d.ts",
8
+ "files": [
9
+ "src/**/*.js",
10
+ "src/**/*.ts",
11
+ "src/**/*.tsx",
12
+ "!src/**/*.spec.js",
13
+ "!src/**/*.spec.tsx",
14
+ "types/**/*.d.ts"
15
+ ],
8
16
  "exports": {
9
- ".": "./src/index.ts",
10
- "./ui/payload": "./src/index.ts",
11
- "./components/*": "./src/components/*",
12
- "./templates": "./src/templates/PayloadCollectionTemplate.js",
13
- "./templates/*": "./src/templates/*",
14
- "./richtext": "./src/richtext/index.js",
15
- "./richtext/client": "./src/richtext/client/index.js",
16
- "./richtext/*": "./src/richtext/*"
17
+ ".": {
18
+ "types": "./types/index.d.ts",
19
+ "import": "./src/index.js"
20
+ },
21
+ "./ui/payload": {
22
+ "types": "./types/index.d.ts",
23
+ "import": "./src/index.js"
24
+ },
25
+ "./components/*": {
26
+ "types": "./types/components/*.d.ts",
27
+ "import": "./src/components/*.js"
28
+ },
29
+ "./templates": {
30
+ "types": "./types/templates/PayloadCollectionTemplate.d.ts",
31
+ "import": "./src/templates/PayloadCollectionTemplate.js"
32
+ },
33
+ "./templates/*": {
34
+ "types": "./types/templates/*.d.ts",
35
+ "import": "./src/templates/*.js"
36
+ },
37
+ "./richtext": {
38
+ "types": "./types/richtext/index.d.ts",
39
+ "import": "./src/richtext/index.js"
40
+ },
41
+ "./richtext/client": {
42
+ "types": "./types/richtext/client/index.d.ts",
43
+ "import": "./src/richtext/client/index.js"
44
+ },
45
+ "./richtext/Nan0HTMLFeature.js": {
46
+ "types": "./types/richtext/Nan0HTMLFeature.d.ts",
47
+ "import": "./src/richtext/Nan0HTMLFeature.js"
48
+ },
49
+ "./richtext/*": {
50
+ "types": "./types/richtext/*.d.ts",
51
+ "import": "./src/richtext/*.js"
52
+ }
17
53
  },
18
54
  "dependencies": {
19
55
  "@payloadcms/richtext-lexical": "3.86.0",
@@ -25,8 +61,20 @@
25
61
  "@payloadcms/ui": "^3.0.0",
26
62
  "react": "^19.0.0"
27
63
  },
64
+ "devDependencies": {
65
+ "typescript": "^5.9.3"
66
+ },
28
67
  "license": "MIT",
29
68
  "scripts": {
30
- "test": "node --test src/test/*.spec.tsx src/templates/*.spec.js"
69
+ "prebuild": "rm -rf types/",
70
+ "build": "tsc",
71
+ "clean": "rm -rf .cache/ && rm -rf dist/",
72
+ "clean:modules": "rm -rf node_modules",
73
+ "test": "node --test \"src/**/*.test.js\"",
74
+ "test:release": "node --test \"src/test/releases/**/*.test.js\"",
75
+ "release:spec": "node --test \"releases/**/*.spec.js\"",
76
+ "knip": "knip --production || true",
77
+ "audit": "pnpm audit --prod || true",
78
+ "test:all": "npm run test && npm run build && npm run test:release && npm run knip && npm run audit"
31
79
  }
32
80
  }
@@ -2,16 +2,9 @@ export { ImageCell } from './components/ImageCell.js'
2
2
  export { BooleanCell } from './components/BooleanCell.js'
3
3
  export { MapCell } from './components/MapCell.js'
4
4
 
5
-
6
- // richtext — pure functions only (node classes require 'lexical' context)
5
+ // richtext — pure functions only & Nan0HTMLFeature
7
6
  export { fromNan0Html, inventoryNan0Html, FORMAT } from './richtext/fromNan0Html.js'
7
+ export { Nan0HTMLFeature } from './richtext/Nan0HTMLFeature.js'
8
8
 
9
9
  // access control helpers
10
10
  export { accessFor, publicAccess } from './access.js'
11
-
12
-
13
-
14
-
15
-
16
-
17
-
@@ -21,6 +21,5 @@ describe('PayloadCollectionTemplate', () => {
21
21
  assert.ok(output.includes("const useAsTitle = 'name'"))
22
22
  assert.ok(output.includes('"uk": "Картка"'))
23
23
  assert.ok(output.includes('"name": "id"'))
24
-
25
24
  })
26
25
  })
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Access control helpers for Payload CMS collections.
3
+ */
4
+ /**
5
+ * Creates a Payload access control function checking user presence and optional role(s).
6
+ *
7
+ * @example
8
+ * // Requires any authenticated user
9
+ * create: accessFor()
10
+ *
11
+ * // Requires specific role(s)
12
+ * update: accessFor('admin', 'editor')
13
+ * // or
14
+ * delete: accessFor(['admin'])
15
+ *
16
+ * @param {...(string | string[])} roles - Allowed role(s). If none specified, checks for any authenticated user.
17
+ * @returns {import('payload').Access} Payload Access control function
18
+ */
19
+ export function accessFor(...roles: (string | string[])[]): import("payload").Access;
20
+ /**
21
+ * Shortcut alias for public read access.
22
+ * @type {() => boolean}
23
+ */
24
+ export const publicAccess: () => boolean;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @typedef {Object} BooleanCellProps
3
+ * @property {boolean} [cellData]
4
+ * @property {boolean} [value]
5
+ * @property {any} [field]
6
+ */
7
+ /**
8
+ * Custom BooleanCell renderer for Payload CMS admin tables.
9
+ * @param {BooleanCellProps} props
10
+ * @returns {React.JSX.Element}
11
+ */
12
+ export function BooleanCell({ cellData, value, field }: BooleanCellProps): React.JSX.Element;
13
+ export default BooleanCell;
14
+ export type BooleanCellProps = {
15
+ cellData?: boolean | undefined;
16
+ value?: boolean | undefined;
17
+ field?: any;
18
+ };
19
+ import React from 'react';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @typedef {Object} ImageCellProps
3
+ * @property {string | { url?: string; thumbnailURL?: string }} [cellData]
4
+ * @property {string | { url?: string; thumbnailURL?: string }} [value]
5
+ */
6
+ /**
7
+ * Image preview cell for Payload CMS admin tables.
8
+ * @param {ImageCellProps} props
9
+ * @returns {React.JSX.Element | null}
10
+ */
11
+ export function ImageCell(props: ImageCellProps): React.JSX.Element | null;
12
+ export default ImageCell;
13
+ export type ImageCellProps = {
14
+ cellData?: string | {
15
+ url?: string;
16
+ thumbnailURL?: string;
17
+ } | undefined;
18
+ value?: string | {
19
+ url?: string;
20
+ thumbnailURL?: string;
21
+ } | undefined;
22
+ };
23
+ import React from 'react';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @typedef {Object} MapCellProps
3
+ * @property {any} [cellData]
4
+ * @property {any} [value]
5
+ * @property {any} [rowData]
6
+ */
7
+ /**
8
+ * Map preview cell for Payload CMS admin tables with OpenStreetMap link.
9
+ * @param {MapCellProps} props
10
+ * @returns {React.JSX.Element}
11
+ */
12
+ export function MapCell({ rowData }: MapCellProps): React.JSX.Element;
13
+ export default MapCell;
14
+ export type MapCellProps = {
15
+ cellData?: any;
16
+ value?: any;
17
+ rowData?: any;
18
+ };
19
+ import React from 'react';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @typedef {Object} AppOptions
3
+ * @property {import('payload').Payload} [payload]
4
+ * @property {any} [config]
5
+ */
6
+ /** @typedef {import('@nan0web/ui').ModelAsAppOptions & AppOptions} PayloadAppOptions */
7
+ /**
8
+ * PayloadApp
9
+ * Base application class for Payload CMS subcommands (SeedModel, TransformModel, etc.)
10
+ * @extends {ModelAsApp}
11
+ */
12
+ export class PayloadApp extends ModelAsApp {
13
+ /**
14
+ * @param {Partial<ModelAsApp> | Record<string, any>} [data={}]
15
+ * @param {Partial<PayloadAppOptions>} [options={}]
16
+ */
17
+ constructor(data?: Partial<ModelAsApp> | Record<string, any>, options?: Partial<PayloadAppOptions>);
18
+ _: {
19
+ payload: import("payload").BasePayload | null;
20
+ config: any;
21
+ adapter: import("@nan0web/ui").InputAdapter;
22
+ parentPath: string;
23
+ _isExplicit: boolean;
24
+ db: import("@nan0web/db").default | null | undefined;
25
+ plugins: Record<string, any>;
26
+ t: import("@nan0web/types/src/utils/TFunction").TFunction;
27
+ };
28
+ /**
29
+ * Lazily initializes and returns the Payload Local API instance.
30
+ * @returns {Promise<import('payload').Payload>}
31
+ */
32
+ getPayloadInstance(): Promise<import("payload").Payload>;
33
+ }
34
+ export type AppOptions = {
35
+ payload?: import("payload").BasePayload | undefined;
36
+ config?: any;
37
+ };
38
+ export type PayloadAppOptions = import("@nan0web/ui").ModelAsAppOptions & AppOptions;
39
+ import { ModelAsApp } from '@nan0web/ui';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SeedModel - Universal Subcommand to seed DB-FS data into Payload CMS
3
+ * @extends {PayloadApp}
4
+ */
5
+ export class SeedModel extends PayloadApp {
6
+ static alias: string;
7
+ static UI: {
8
+ title: string;
9
+ start: string;
10
+ scanning: string;
11
+ loading: string;
12
+ seeded: string;
13
+ done: string;
14
+ errorDb: string;
15
+ };
16
+ static dataDir: {
17
+ help: string;
18
+ default: string;
19
+ positional: boolean;
20
+ };
21
+ static output: {
22
+ help: string;
23
+ default: string;
24
+ alias: string;
25
+ };
26
+ /**
27
+ * @param {Partial<SeedModel>} [data={}]
28
+ * @param {Partial<import('./PayloadApp.js').PayloadAppOptions>} [options={}]
29
+ */
30
+ constructor(data?: Partial<SeedModel>, options?: Partial<import("./PayloadApp.js").PayloadAppOptions>);
31
+ /** @type {string} Target directory containing SSOT data files */ dataDir: string;
32
+ /** @type {string} Output path */ output: string;
33
+ /**
34
+ * Reads seed files via DB instance.
35
+ * @param {string} target
36
+ * @returns {Promise<string[]>}
37
+ */
38
+ readSeedFiles(target: string): Promise<string[]>;
39
+ /**
40
+ * Dynamically resolves collection slug from document metadata or URI structure.
41
+ * @param {string} uri
42
+ * @param {any} [doc]
43
+ * @returns {string}
44
+ */
45
+ resolveCollectionSlug(uri: string, doc?: any): string;
46
+ /**
47
+ * Base default normalization for DB-FS records.
48
+ * Can be overridden by domain-specific SeedModels (e.g. CardSeedModel, BankSeedModel).
49
+ * @param {any} rawRecord
50
+ * @returns {Object}
51
+ */
52
+ normalizeRecord(rawRecord: any): any;
53
+ /**
54
+ * @returns {AsyncGenerator<import('@nan0web/ui/core').Intent, import('@nan0web/ui/core').ResultIntent, any>}
55
+ */
56
+ run(): AsyncGenerator<import("@nan0web/ui/core").Intent, import("@nan0web/ui/core").ResultIntent, any>;
57
+ }
58
+ import { PayloadApp } from './PayloadApp.js';
@@ -0,0 +1,6 @@
1
+ export { ImageCell } from "./components/ImageCell.js";
2
+ export { BooleanCell } from "./components/BooleanCell.js";
3
+ export { MapCell } from "./components/MapCell.js";
4
+ export { Nan0HTMLFeature } from "./richtext/Nan0HTMLFeature.js";
5
+ export { fromNan0Html, inventoryNan0Html, FORMAT } from "./richtext/fromNan0Html.js";
6
+ export { accessFor, publicAccess } from "./access.js";
@@ -0,0 +1,29 @@
1
+ export function $createNan0ComponentNode({ component, props }: {
2
+ component: any;
3
+ props?: {} | undefined;
4
+ }): Nan0ComponentNode;
5
+ export function $isNan0ComponentNode(node: any): node is Nan0ComponentNode;
6
+ export class Nan0ComponentNode extends DecoratorNode<any> {
7
+ static clone(node: any): Nan0ComponentNode;
8
+ static importJSON(serializedNode: any): Nan0ComponentNode;
9
+ constructor({ component, props, key }: {
10
+ component: any;
11
+ props?: {} | undefined;
12
+ key: any;
13
+ });
14
+ /** @type {string} */ __component: string;
15
+ /** @type {Record<string, any>} */ __props: Record<string, any>;
16
+ exportJSON(): {
17
+ type: string;
18
+ version: number;
19
+ component: string;
20
+ props: Record<string, any>;
21
+ $?: Record<string, unknown>;
22
+ };
23
+ getComponent(): string;
24
+ getProps(): Record<string, any>;
25
+ createDOM(): HTMLDivElement;
26
+ updateDOM(): boolean;
27
+ decorate(): null;
28
+ }
29
+ import { DecoratorNode } from 'lexical';
@@ -0,0 +1,34 @@
1
+ export function $createNan0ElementNode({ tag, attributes }: {
2
+ tag: any;
3
+ attributes?: {} | undefined;
4
+ }): Nan0ElementNode;
5
+ export function $isNan0ElementNode(node: any): node is Nan0ElementNode;
6
+ export class Nan0ElementNode extends ElementNode {
7
+ static clone(node: any): Nan0ElementNode;
8
+ static importJSON(serializedNode: any): Nan0ElementNode;
9
+ constructor({ tag, attributes, key }: {
10
+ tag: any;
11
+ attributes?: {} | undefined;
12
+ key: any;
13
+ });
14
+ /** @type {string} */ __tag: string;
15
+ /** @type {Record<string, any>} */ __attributes: Record<string, any>;
16
+ exportJSON(): {
17
+ type: string;
18
+ version: number;
19
+ tag: string;
20
+ attributes: Record<string, any>;
21
+ $?: Record<string, unknown> | undefined;
22
+ children: import("lexical").SerializedLexicalNode[];
23
+ direction: "ltr" | "rtl" | null;
24
+ format: import("lexical").ElementFormatType;
25
+ indent: number;
26
+ textFormat?: number;
27
+ textStyle?: string;
28
+ };
29
+ getTag(): string;
30
+ getAttributes(): Record<string, any>;
31
+ createDOM(config: any): HTMLElement;
32
+ updateDOM(): boolean;
33
+ }
34
+ import { ElementNode } from 'lexical';
@@ -0,0 +1,2 @@
1
+ export const Nan0HTMLFeature: import("@payloadcms/richtext-lexical").FeatureProviderProviderServer<undefined, undefined, undefined>;
2
+ export default Nan0HTMLFeature;
@@ -0,0 +1,41 @@
1
+ export function $createNan0RawNode({ source }: {
2
+ source: any;
3
+ }): Nan0RawNode;
4
+ export function $isNan0RawNode(node: any): node is Nan0RawNode;
5
+ export class Nan0RawNode extends ElementNode {
6
+ static clone(node: any): Nan0RawNode;
7
+ static importJSON(serializedNode: any): Nan0RawNode;
8
+ constructor({ source, key }: {
9
+ source: any;
10
+ key: any;
11
+ });
12
+ /** @type {{ tag: string, attributes: Record<string, any>, children: any[] }} */ __source: {
13
+ tag: string;
14
+ attributes: Record<string, any>;
15
+ children: any[];
16
+ };
17
+ exportJSON(): {
18
+ type: string;
19
+ version: number;
20
+ source: {
21
+ tag: string;
22
+ attributes: Record<string, any>;
23
+ children: any[];
24
+ };
25
+ $?: Record<string, unknown> | undefined;
26
+ children: import("lexical").SerializedLexicalNode[];
27
+ direction: "ltr" | "rtl" | null;
28
+ format: import("lexical").ElementFormatType;
29
+ indent: number;
30
+ textFormat?: number;
31
+ textStyle?: string;
32
+ };
33
+ getSource(): {
34
+ tag: string;
35
+ attributes: Record<string, any>;
36
+ children: any[];
37
+ };
38
+ createDOM(config: any): HTMLDivElement;
39
+ updateDOM(): boolean;
40
+ }
41
+ import { ElementNode } from 'lexical';
@@ -0,0 +1,11 @@
1
+ export function Nan0HTMLFeatureClient(props?: {}): {
2
+ clientFeatureProps: {};
3
+ feature: () => {
4
+ nodes: (typeof Nan0ElementNode | typeof Nan0RawNode | typeof Nan0ComponentNode)[];
5
+ sanitizedClientFeatureProps: {};
6
+ };
7
+ };
8
+ export default Nan0HTMLFeatureClient;
9
+ import { Nan0ElementNode } from '../Nan0ElementNode.js';
10
+ import { Nan0RawNode } from '../Nan0RawNode.js';
11
+ import { Nan0ComponentNode } from '../Nan0ComponentNode.js';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Recursively walk the NaN0HTML AST and collect unknown constructs.
3
+ * @param {any} node
4
+ * @param {Array<{tag: string, attributes: Object, parent: string}>} out
5
+ * @param {string} parent
6
+ */
7
+ export function inventoryNan0Html(node: any, out?: Array<{
8
+ tag: string;
9
+ attributes: any;
10
+ parent: string;
11
+ }>, parent?: string): {
12
+ tag: string;
13
+ attributes: any;
14
+ parent: string;
15
+ }[];
16
+ /**
17
+ * Convert a NaN0HTML AST into a Payload Lexical root state.
18
+ * @param {any} content NaN0HTML AST (array of blocks, or a single block)
19
+ * @returns {{ root: { type: string, version: number, children: any[] } }}
20
+ */
21
+ export function fromNan0Html(content: any): {
22
+ root: {
23
+ type: string;
24
+ version: number;
25
+ children: any[];
26
+ };
27
+ };
28
+ /**
29
+ * fromNan0Html
30
+ * Converts a NaN0HTML AST (page.content / content) into a Payload Lexical state JSON.
31
+ *
32
+ * NaN0HTML AST shape:
33
+ * - array → sequence of blocks
34
+ * - string → text content
35
+ * - object → element with keys: `$attr` for attributes, lowercase tag keys for
36
+ * child elements, `Uppercase.With.Dot` keys for NaN0 components
37
+ * - `tag: true` → void element (e.g. `br`, `hr`)
38
+ *
39
+ * Strategy:
40
+ * - Clean elements (no attributes) map to native Lexical nodes where possible.
41
+ * - Elements carrying any `$`-attribute are preserved losslessly as a `nan0-element`
42
+ * node (tag + attributes + children) so round-trip back to NaN0HTML is lossless.
43
+ * - Unknown tags fall back to a `nan0-raw` node and are recorded in the inventory.
44
+ * - NaN0 components (`App.Header`, `Card.Details`, ...) become `nan0-component` nodes.
45
+ *
46
+ * The converter returns a plain structure; localization is handled at the Payload
47
+ * collection level, not here.
48
+ */
49
+ export const FORMAT: Readonly<{
50
+ bold: number;
51
+ italic: number;
52
+ underline: number;
53
+ strikethrough: number;
54
+ }>;
55
+ export default fromNan0Html;
@@ -0,0 +1,2 @@
1
+ export { default as toNan0Html } from "./toNan0Html.js";
2
+ export { default as fromNan0Html, inventoryNan0Html, FORMAT } from "./fromNan0Html.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Convert Payload Lexical state (or root node) back into a NaN0HTML AST.
3
+ * @param {Object} lexicalState Lexical state object `{ root: { children: [...] } }` or node object
4
+ * @returns {any} NaN0HTML AST structure (Array, Object, or String)
5
+ */
6
+ export function toNan0Html(lexicalState: any): any;
7
+ export default toNan0Html;
@@ -0,0 +1,58 @@
1
+ /** @typedef {Object} Field */
2
+ /**
3
+ * PayloadCollectionTemplate - Model to generate Payload CMS CollectionConfig using CodeTemplate.
4
+ */
5
+ export class PayloadCollectionTemplate extends Model {
6
+ static alias: string;
7
+ static collectionSlug: {
8
+ help: string;
9
+ default: string;
10
+ };
11
+ static useAsTitle: {
12
+ help: string;
13
+ default: string;
14
+ };
15
+ static labels: {
16
+ help: string;
17
+ default: {
18
+ singular: string;
19
+ plural: string;
20
+ };
21
+ };
22
+ static group: {
23
+ help: string;
24
+ type: string;
25
+ default: string;
26
+ };
27
+ static fields: {
28
+ help: string;
29
+ default: never[];
30
+ };
31
+ static template: {
32
+ help: string;
33
+ default: string;
34
+ };
35
+ /**
36
+ * @param {Partial<PayloadCollectionTemplate>} [data={}]
37
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
38
+ */
39
+ constructor(data?: Partial<PayloadCollectionTemplate>, options?: Partial<import("@nan0web/types").ModelOptions>);
40
+ /** @type {string} Collection slug */ collectionSlug: string;
41
+ /** @type {string} Label/title field name */ useAsTitle: string;
42
+ /** @type {Object} Labels object */ labels: any;
43
+ /** @type {Object|string} Group object or string */ group: any | string;
44
+ /** @type {Array<Field>} Collection fields */ fields: Array<Field>;
45
+ /** @type {Object} Custom replace snippets */ snippets: any;
46
+ /**
47
+ * Compiles the CollectionConfig template using native CodeTemplate replace blocks.
48
+ * @returns {Promise<string>} Generated TS code for the collection
49
+ */
50
+ compile(): Promise<string>;
51
+ /**
52
+ * Synchronously compiles the CollectionConfig template.
53
+ * @returns {string} Generated TS code for the collection
54
+ */
55
+ compileSync(): string;
56
+ }
57
+ export type Field = any;
58
+ import { Model } from '@nan0web/types';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Polymorphic Registry for Payload CMS UI block views.
3
+ */
4
+ export const uiPayloadRegistry: Map<any, any>;
@@ -1,64 +0,0 @@
1
- ---
2
- version: 3.3.0
3
- type: feature
4
- status: active
5
- locale: uk
6
- models: ["MarkdownBlockView", "MediaBlockView"]
7
- ---
8
-
9
- # 🚀 Mission: Payload CMS UI Адаптери та Блок-Компоненти у @nan0web/ui-payload (v3.3.0)
10
-
11
- ## 🏁 Overview (Огляд)
12
- Реалізація універсальних UI компонентів та адаптерів Payload CMS для `MarkdownBlock`, `MediaBlock` та надання поліморфного `uiPayloadRegistry`.
13
-
14
- ## 👥 User Stories (Сценарії)
15
- - Як розробник, я хочу відображати блоки `MarkdownBlock` через готові React/Payload UI елементи з підтримкою вкладеного графіків/калькуляторів.
16
- - Як розробник, я хочу реєструвати Payload UI блоки через `uiPayloadRegistry`, щоб OLMUI рендеринг працював без ручних процедурних розгалужень.
17
-
18
- ## 🏗 Data-Driven Architecture (Моделювання)
19
- - `uiPayloadRegistry`:
20
- - `Map<ModelConstructor, PayloadBlockViewComponent>`
21
- - `MarkdownBlockView`:
22
- - Компонент для `type: 'text/markdown'`
23
- - `MediaBlockView`:
24
- - Компонент для `static $upload = true`
25
-
26
- ## 🎯 Scope (Задачі)
27
- - [ ] Створити `uiPayloadRegistry` для прив'язки моделей до Payload/React UI View класів.
28
- - [ ] Створити універсальний компонент `MarkdownBlockView`.
29
- - [ ] Створити універсальний компонент `MediaBlockView`.
30
-
31
- ## ✅ Acceptance Criteria (DoD)
32
- - [ ] Контрактні тести (`task.spec.js`) написані і успішно проходять (Green).
33
- - [ ] OLMUI рендеринг: 0% процедурних `switch/case` в елементах рендерингу.
34
-
35
- ## 📌 Context Checkpoint (2026-08-07)
36
-
37
- ### Implemented
38
- - Added `fromNan0Html()` and `inventoryNan0Html()` conversion helpers.
39
- - Added Lexical custom nodes: `Nan0ElementNode`, `Nan0RawNode`, and `Nan0ComponentNode`.
40
- - Added `Nan0HTMLFeature()` with server-side HTML converters and client import-map entry.
41
- - Added Payload Lexical dependencies and public richtext exports.
42
- - Added `ImageCell` preview with fixed `192x108` dimensions.
43
- - Added Payload 3.86-compatible client feature provider in `src/richtext/client/index.js`.
44
-
45
- ### Verification
46
- - `pnpm --prefix packages/ui-payload test` passes.
47
- - Root commit hook passed 43 tests.
48
- - Industrial Bank seed and Payload admin integration were exercised.
49
-
50
- ### Current State
51
- - Changes are intentionally uncommitted after removing the last agent-created commit.
52
- - They are staged in `packages/ui-payload` and must be committed by the user.
53
- - Other repository changes are unrelated and must not be included.
54
- - Industrial Bank CMS import map uses `@nan0web/ui-payload#ImageCell`.
55
- - CMS image paths `/img/...` and `/images/...` are mounted from `bank/public`.
56
-
57
- ### Next Steps
58
- 1. Commit only the staged `packages/ui-payload` changes.
59
- 2. Run the Industrial Bank CMS build and fix remaining unrelated TypeScript blockers, starting with `src/test-read.ts` using obsolete `DB({ rootDir })`.
60
- 3. Verify `/admin/collections/cards/<id>` editing and `ImageCell` rendering.
61
- 4. Add focused contract tests for Nan0HTML/Lexical conversion and custom nodes.
62
-
63
- ### New Chat Bootstrap
64
- Start from branch `feature/payload-cms`, base `070afe7`, with staged changes in `packages/ui-payload`. Do not reset, clean, commit, or push without explicit user instruction.
@@ -1,13 +0,0 @@
1
- import { describe, it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { uiPayloadRegistry } from '../../../../src/uiPayloadRegistry.js'
4
-
5
- describe('Release v3.3.0: @nan0web/ui-payload Contract', () => {
6
- it('uiPayloadRegistry provides polymorphic lookup for block views', () => {
7
- class MockModel {}
8
- class MockView {}
9
- uiPayloadRegistry.set(MockModel, MockView)
10
-
11
- assert.equal(uiPayloadRegistry.get(MockModel), MockView)
12
- })
13
- })
@@ -1,8 +0,0 @@
1
- import React from 'react'
2
-
3
- export function BooleanCell(props) {
4
- const val = Boolean(props.cellData ?? props.value)
5
- return React.createElement('span', {
6
- style: { color: val ? '#22c55e' : '#ef4444', fontWeight: 600 },
7
- }, val ? '✓' : '✗')
8
- }
@@ -1,50 +0,0 @@
1
- 'use client'
2
-
3
- import React from 'react'
4
- import { useTranslation } from '@payloadcms/ui'
5
-
6
- export const BooleanCell: React.FC<{ cellData: boolean; field: any }> = ({ cellData, field }) => {
7
- const { i18n } = useTranslation()
8
- const lang = i18n?.language?.toLowerCase().startsWith('en') ? 'en' : 'uk'
9
- const val = Boolean(cellData)
10
-
11
- const isHiddenField = field?.name?.toLowerCase() === 'hidden'
12
- const customLabels = field?.admin?.custom?.labels
13
-
14
- const defaultLabels = isHiddenField
15
- ? { true: { uk: '🙈 Приховано', en: '🙈 Hidden' }, false: { uk: '🌐 Активно', en: '🌐 Active' } }
16
- : { true: { uk: '✅ Так', en: '✅ Yes' }, false: { uk: '❌ Ні', en: '❌ No' } }
17
-
18
- const labels = customLabels || defaultLabels
19
- const text = labels[val ? 'true' : 'false']?.[lang] || (val ? 'Yes' : 'No')
20
-
21
- const isWarning = isHiddenField ? val : !val
22
- const variant = isWarning ? 'warning' : 'success'
23
-
24
- return (
25
- <span
26
- className={`badge bg-${variant}-subtle text-${variant}-emphasis border border-${variant}-subtle`}
27
- style={{
28
- display: 'inline-flex',
29
- alignItems: 'center',
30
- gap: '4px',
31
- padding: '3px 8px',
32
- borderRadius: '12px',
33
- fontSize: '11px',
34
- fontWeight: 600,
35
- backgroundColor: isWarning
36
- ? 'var(--bs-warning-bg-subtle, rgba(255, 193, 7, 0.15))'
37
- : 'var(--bs-success-bg-subtle, rgba(25, 135, 84, 0.15))',
38
- color: isWarning
39
- ? 'var(--bs-warning-text-emphasis, #664d03)'
40
- : 'var(--bs-success-text-emphasis, #0a3622)',
41
- borderColor: isWarning
42
- ? 'var(--bs-warning-border-subtle, rgba(255, 193, 7, 0.3))'
43
- : 'var(--bs-success-border-subtle, rgba(25, 135, 84, 0.3))',
44
- }}
45
- suppressHydrationWarning
46
- >
47
- {text}
48
- </span>
49
- )
50
- }
@@ -1,26 +0,0 @@
1
- import React from 'react'
2
-
3
- export function ImageCell(props) {
4
- const value = props.cellData || props.value
5
- if (!value) return null
6
-
7
- const srcValue = typeof value === 'object' ? value.url || value.thumbnailURL : value
8
- if (!srcValue) return null
9
- const src = srcValue.startsWith('http') || srcValue.startsWith('/') ? srcValue : `/${srcValue}`
10
-
11
- return React.createElement('img', {
12
- src,
13
- alt: 'Thumbnail',
14
- width: 192,
15
- height: 108,
16
- style: {
17
- width: 192,
18
- minWidth: 192,
19
- height: 108,
20
- minHeight: 108,
21
- objectFit: 'cover',
22
- borderRadius: 6,
23
- display: 'block',
24
- },
25
- })
26
- }
@@ -1,54 +0,0 @@
1
- 'use client'
2
-
3
- import React, { useState } from 'react'
4
-
5
- export const ImageCell: React.FC<{ cellData: string }> = ({ cellData }) => {
6
- const [hasError, setHasError] = useState(false)
7
-
8
- if (!cellData || typeof cellData !== 'string' || hasError) {
9
- return (
10
- <div
11
- style={{
12
- display: 'inline-flex',
13
- alignItems: 'center',
14
- justifyContent: 'center',
15
- width: '192px',
16
- height: '108px',
17
- borderRadius: '6px',
18
- background: 'rgba(255, 255, 255, 0.05)',
19
- border: '1px solid rgba(255, 255, 255, 0.1)',
20
- fontSize: '12px',
21
- color: '#888',
22
- }}
23
- suppressHydrationWarning
24
- >
25
- <span>🖼️ Без фото</span>
26
- </div>
27
- )
28
- }
29
-
30
- const src = cellData.startsWith('http') || cellData.startsWith('/') ? cellData : `/${cellData}`
31
-
32
- return (
33
- <div style={{ display: 'inline-flex', alignItems: 'center' }} suppressHydrationWarning>
34
- <img
35
- src={src}
36
- alt="Preview"
37
- style={{
38
- width: '192px',
39
- minWidth: '192px',
40
- height: '108px',
41
- minHeight: '108px',
42
- display: 'block',
43
- objectFit: 'cover',
44
- borderRadius: '6px',
45
- border: '1px solid rgba(255, 255, 255, 0.25)',
46
- background: '#111',
47
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.4)',
48
- }}
49
- onError={() => setHasError(true)}
50
- suppressHydrationWarning
51
- />
52
- </div>
53
- )
54
- }
@@ -1,6 +0,0 @@
1
- import React from 'react'
2
-
3
- export function MapCell(props) {
4
- const val = props.cellData || props.value
5
- return React.createElement('span', null, String(val || ''))
6
- }
@@ -1,59 +0,0 @@
1
- 'use client'
2
-
3
- import React from 'react'
4
-
5
- export const MapCell: React.FC<{ rowData: any }> = ({ rowData }) => {
6
- const lat = rowData?.lat || rowData?.latitude || '50.4501'
7
- const lon = rowData?.lng || rowData?.longitude || '30.5234'
8
- const address = rowData?.address || rowData?.title || 'Відділення'
9
-
10
- const zoom = 15
11
- const latNum = parseFloat(String(lat)) || 50.4501
12
- const lonNum = parseFloat(String(lon)) || 30.5234
13
-
14
- const x = Math.floor(((lonNum + 180) / 360) * Math.pow(2, zoom))
15
- const y = Math.floor(
16
- ((1 - Math.log(Math.tan((latNum * Math.PI) / 180) + 1 / Math.cos((latNum * Math.PI) / 180)) / Math.PI) / 2) *
17
- Math.pow(2, zoom)
18
- )
19
-
20
- const osmTileUrl = `https://tile.openstreetmap.org/${zoom}/${x}/${y}.png`
21
- const osmMapUrl = `https://www.openstreetmap.org/?mlat=${latNum}&mlon=${lonNum}#map=16/${latNum}/${lonNum}`
22
-
23
- return (
24
- <a
25
- href={osmMapUrl}
26
- target="_blank"
27
- rel="noopener noreferrer"
28
- style={{
29
- display: 'inline-flex',
30
- alignItems: 'center',
31
- gap: '8px',
32
- textDecoration: 'none',
33
- color: 'inherit',
34
- }}
35
- suppressHydrationWarning
36
- >
37
- <img
38
- src={osmTileUrl}
39
- alt="OpenStreetMap"
40
- style={{
41
- width: '100px',
42
- height: '56px',
43
- objectFit: 'cover',
44
- borderRadius: '4px',
45
- border: '1px solid rgba(255, 255, 255, 0.2)',
46
- background: '#222',
47
- }}
48
- onError={(e) => {
49
- ;(e.target as HTMLElement).style.display = 'none'
50
- }}
51
- suppressHydrationWarning
52
- />
53
- <div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
54
- <span style={{ fontSize: '12px', fontWeight: 600 }}>📍 {address}</span>
55
- <span style={{ fontSize: '10px', color: '#1471d1' }}>🗺️ OpenStreetMap ({latNum.toFixed(4)}, {lonNum.toFixed(4)})</span>
56
- </div>
57
- </a>
58
- )
59
- }
@@ -1,63 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { fromNan0Html } from './fromNan0Html.js'
3
- import { toNan0Html } from './toNan0Html.js'
4
-
5
- describe('NaN0HTML ↔ Lexical Converter Round-trip', () => {
6
- it('converts basic paragraphs and text formats', () => {
7
- const ast = { p: [{ strong: 'Bold text' }, ' and ', { em: 'italic text' }] }
8
- const lexical = fromNan0Html(ast)
9
- expect(lexical.root.type).toBe('root')
10
- expect(lexical.root.children.length).toBe(1)
11
- expect(lexical.root.children[0].type).toBe('paragraph')
12
-
13
- const backToAst = toNan0Html(lexical)
14
- expect(backToAst).toEqual(ast)
15
- })
16
-
17
- it('converts headings correctly', () => {
18
- const ast = { h2: 'Title Heading' }
19
- const lexical = fromNan0Html(ast)
20
- expect(lexical.root.children[0].type).toBe('heading')
21
- expect(lexical.root.children[0].tag).toBe('h2')
22
-
23
- const backToAst = toNan0Html(lexical)
24
- expect(backToAst).toEqual(ast)
25
- })
26
-
27
- it('converts lists and items', () => {
28
- const ast = { ul: [{ li: 'Item 1' }, { li: 'Item 2' }] }
29
- const lexical = fromNan0Html(ast)
30
- expect(lexical.root.children[0].type).toBe('list')
31
- expect(lexical.root.children[0].listType).toBe('bullet')
32
-
33
- const backToAst = toNan0Html(lexical)
34
- expect(backToAst).toEqual(ast)
35
- })
36
-
37
- it('converts links with attributes', () => {
38
- const ast = { a: 'Click here', $href: 'https://example.com', $target: '_blank' }
39
- const lexical = fromNan0Html(ast)
40
-
41
- const backToAst = toNan0Html(lexical)
42
- expect(backToAst).toEqual({ a: 'Click here', $href: 'https://example.com', $target: '_blank' })
43
- })
44
-
45
- it('preserves nan0-component losslessly', () => {
46
- const ast = { 'Card.Details': { card: '$' } }
47
- const lexical = fromNan0Html(ast)
48
- expect(lexical.root.children[0].type).toBe('nan0-component')
49
- expect(lexical.root.children[0].component).toBe('Card.Details')
50
-
51
- const backToAst = toNan0Html(lexical)
52
- expect(backToAst).toEqual(ast)
53
- })
54
-
55
- it('preserves unknown tags as nan0-raw', () => {
56
- const ast = { 'custom-widget': 'Widget content', $mode: 'test' }
57
- const lexical = fromNan0Html(ast)
58
- expect(lexical.root.children[0].type).toBe('nan0-raw')
59
-
60
- const backToAst = toNan0Html(lexical)
61
- expect(backToAst).toEqual({ 'custom-widget': 'Widget content', $mode: 'test' })
62
- })
63
- })