@nan0web/ui-payload 3.3.0

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025, ЯRаСлав (YaRaSLove) <support@yaro.page>
4
+
5
+ Permission to use, copy, modify, and distribute this software for any purpose
6
+ with or without fee is hereby granted, provided that the above copyright notice
7
+ and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@nan0web/ui-payload",
3
+ "version": "3.3.0",
4
+ "description": "NaN0Web Universal Payload CMS UI Components Package (ImageCell, MapCell, BooleanCell, richtext)",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "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
+ "dependencies": {
19
+ "@payloadcms/richtext-lexical": "3.86.0",
20
+ "lexical": "0.41.0",
21
+ "react": "^19.0.0"
22
+ },
23
+ "peerDependencies": {
24
+ "payload": "^3.0.0",
25
+ "@payloadcms/ui": "^3.0.0",
26
+ "react": "^19.0.0"
27
+ },
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "test": "node --test src/test/*.spec.tsx src/templates/*.spec.js"
31
+ }
32
+ }
@@ -0,0 +1,64 @@
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.
@@ -0,0 +1,13 @@
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
+ })
package/src/access.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Access control helpers for Payload CMS collections.
3
+ */
4
+
5
+ /**
6
+ * Creates a Payload access control function checking user presence and optional role(s).
7
+ *
8
+ * @example
9
+ * // Requires any authenticated user
10
+ * create: accessFor()
11
+ *
12
+ * // Requires specific role(s)
13
+ * update: accessFor('admin', 'editor')
14
+ * // or
15
+ * delete: accessFor(['admin'])
16
+ *
17
+ * @param {...(string | string[])} roles - Allowed role(s). If none specified, checks for any authenticated user.
18
+ * @returns {import('payload').Access} Payload Access control function
19
+ */
20
+ export function accessFor(...roles) {
21
+ const flatRoles = roles.flat().filter(Boolean)
22
+ return (/** @type {any} */ { req: { user } }) => {
23
+ if (!user) return false
24
+ if (flatRoles.length === 0) return true
25
+ if (Array.isArray(user.roles)) {
26
+ return flatRoles.some((role) => user.roles.includes(role))
27
+ }
28
+ if (typeof user.role === 'string') {
29
+ return flatRoles.includes(user.role)
30
+ }
31
+ return false
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Shortcut alias for public read access.
37
+ * @type {() => boolean}
38
+ */
39
+ export const publicAccess = () => true
@@ -0,0 +1,8 @@
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
+ }
@@ -0,0 +1,50 @@
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
+ }
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,54 @@
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
+ }
@@ -0,0 +1,6 @@
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
+ }
@@ -0,0 +1,59 @@
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
+ }
@@ -0,0 +1,48 @@
1
+ import { ModelAsApp } from '@nan0web/ui'
2
+ import { getPayload } from 'payload'
3
+
4
+ /**
5
+ * @typedef {Object} AppOptions
6
+ * @property {import('payload').Payload} [payload]
7
+ * @property {any} [config]
8
+ */
9
+ /** @typedef {import('@nan0web/ui').ModelAsAppOptions & AppOptions} PayloadAppOptions */
10
+
11
+ /**
12
+ * PayloadApp
13
+ * Base application class for Payload CMS subcommands (SeedModel, TransformModel, etc.)
14
+ * @extends {ModelAsApp}
15
+ */
16
+ export class PayloadApp extends ModelAsApp {
17
+ /**
18
+ * @param {Partial<ModelAsApp> | Record<string, any>} [data={}]
19
+ * @param {Partial<PayloadAppOptions>} [options={}]
20
+ */
21
+ constructor(data = {}, options = {}) {
22
+ super(data, options)
23
+ /** @type {boolean} Show help */ this.help
24
+ this._ = {
25
+ ...this._,
26
+ payload: options.payload || null,
27
+ config: options.config || null,
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Lazily initializes and returns the Payload Local API instance.
33
+ * @returns {Promise<import('payload').Payload>}
34
+ */
35
+ async getPayloadInstance() {
36
+ if (this._.payload) {
37
+ return this._.payload
38
+ }
39
+ if (this._.config) {
40
+ const resolvedConfig = await Promise.resolve(this._.config)
41
+ this._.payload = await getPayload({ config: resolvedConfig })
42
+ return this._.payload
43
+ }
44
+ throw new Error(
45
+ 'PayloadApp: Neither payload instance nor config was provided in options.'
46
+ )
47
+ }
48
+ }
@@ -0,0 +1,149 @@
1
+ import { PayloadApp } from './PayloadApp.js'
2
+ import { show, progress, result } from '@nan0web/ui/core'
3
+
4
+ /**
5
+ * SeedModel - Universal Subcommand to seed DB-FS data into Payload CMS
6
+ * @extends {PayloadApp}
7
+ */
8
+ export class SeedModel extends PayloadApp {
9
+ static alias = 'seed'
10
+
11
+ static UI = {
12
+ title: 'Seed DB-FS Data into Payload CMS',
13
+ start: 'Starting Payload CMS data seeding...',
14
+ scanning: 'Scanning data directory {target}...',
15
+ loading: 'Loading data from {uri}...',
16
+ seeded: 'Seeded data for {collection} -> {file}',
17
+ done: 'Data seeding completed successfully! Seeded {count} records.',
18
+ errorDb: 'No DB instance found. Cannot run SeedModel command.',
19
+ }
20
+
21
+ static dataDir = {
22
+ help: 'Target directory containing SSOT data files (.yaml, .nano, .json)',
23
+ default: 'data',
24
+ positional: true,
25
+ }
26
+
27
+ static output = {
28
+ help: 'Output web directory containing payload.config.ts',
29
+ default: 'web',
30
+ alias: 'o',
31
+ }
32
+
33
+ /**
34
+ * @param {Partial<SeedModel>} [data={}]
35
+ * @param {Partial<import('./PayloadApp.js').PayloadAppOptions>} [options={}]
36
+ */
37
+ constructor(data = {}, options = {}) {
38
+ super(data, options)
39
+ /** @type {string} Target directory containing SSOT data files */ this.dataDir
40
+ /** @type {string} Output path */ this.output
41
+ /** @type {boolean} Help flag */ this.help
42
+ }
43
+
44
+ /**
45
+ * Reads seed files via DB instance.
46
+ * @param {string} target
47
+ * @returns {Promise<string[]>}
48
+ */
49
+ async readSeedFiles(target) {
50
+ const db = this._.db
51
+ if (!db) return []
52
+ let prefix = ''
53
+ if (target && target !== '.') {
54
+ prefix = target.startsWith('@') ? target : `@cwd/${target}`
55
+ }
56
+ const items = []
57
+ try {
58
+ for await (const entry of db.browse(prefix)) {
59
+ if (db.Directory.isConfig(entry.path)) continue
60
+ if (db.Directory.isDirectory(entry.path)) continue
61
+ const ext = entry.path.split('.').pop()?.toLowerCase()
62
+ if (['yaml', 'nan0', 'json', 'yml'].includes(ext || '')) {
63
+ items.push(entry.path)
64
+ }
65
+ }
66
+ } catch (e) {
67
+ // Fallback
68
+ }
69
+ return items
70
+ }
71
+
72
+ /**
73
+ * Dynamically resolves collection slug from document metadata or URI structure.
74
+ * @param {string} uri
75
+ * @param {any} [doc]
76
+ * @returns {string}
77
+ */
78
+ resolveCollectionSlug(uri, doc = {}) {
79
+ if (doc && typeof doc === 'object') {
80
+ if (doc.$collection) return String(doc.$collection)
81
+ if (doc.model && typeof doc.model === 'string') return doc.model.toLowerCase() + 's'
82
+ }
83
+ const parts = uri.split('/').filter(Boolean)
84
+ return parts[parts.length - 2] || 'items'
85
+ }
86
+
87
+ /**
88
+ * Base default normalization for DB-FS records.
89
+ * Can be overridden by domain-specific SeedModels (e.g. CardSeedModel, BankSeedModel).
90
+ * @param {any} rawRecord
91
+ * @returns {Object}
92
+ */
93
+ normalizeRecord(rawRecord) {
94
+ const entity = rawRecord.page || rawRecord.doc || rawRecord
95
+ return {
96
+ title: entity.title || rawRecord.title || '',
97
+ slug: entity.slug || rawRecord.slug || '',
98
+ description: entity.description || rawRecord.description || '',
99
+ image: entity.image || rawRecord.image || '',
100
+ order: entity.order ?? rawRecord.order ?? 0,
101
+ }
102
+ }
103
+
104
+ /**
105
+ * @returns {AsyncGenerator<import('@nan0web/ui/core').Intent, import('@nan0web/ui/core').ResultIntent, any>}
106
+ */
107
+ async *run() {
108
+ const { db, t } = this._
109
+ if (!db) {
110
+ throw new Error(t(SeedModel.UI.errorDb))
111
+ }
112
+
113
+ const payload = await this.getPayloadInstance().catch(() => this._.payload)
114
+ const target = this.dataDir || 'data'
115
+
116
+ yield progress(t(SeedModel.UI.start))
117
+ yield progress(t(SeedModel.UI.scanning, { target }))
118
+
119
+ const items = await this.readSeedFiles(target)
120
+ let count = 0
121
+ let i = 0
122
+ for (const uri of items) {
123
+ yield progress(t(SeedModel.UI.loading, { uri }), i++, items.length)
124
+ const doc = await db.fetch(uri)
125
+ if (doc) {
126
+ const collection = this.resolveCollectionSlug(uri, doc)
127
+ if (payload && typeof payload.create === 'function') {
128
+ try {
129
+ const rawList = doc.departments || doc.currencies || doc.metals || doc.news || (Array.isArray(doc) ? doc : [doc])
130
+ for (const rawRecord of rawList) {
131
+ const normalizedRecord = this.normalizeRecord(rawRecord)
132
+ await payload.create({ collection, data: normalizedRecord })
133
+ ++count
134
+ }
135
+ } catch (e) {
136
+ // Skip fallback record
137
+ }
138
+ } else {
139
+ const rawList = doc.departments || doc.currencies || doc.metals || doc.news || (Array.isArray(doc) ? doc : [doc])
140
+ count += rawList.length
141
+ }
142
+ }
143
+ yield progress(t(SeedModel.UI.loading, { uri }), i, items.length)
144
+ }
145
+
146
+ yield show(t(SeedModel.UI.done, { count }), 'success')
147
+ return result({ status: 'ok', count })
148
+ }
149
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { ImageCell } from './components/ImageCell.js'
2
+ export { BooleanCell } from './components/BooleanCell.js'
3
+ export { MapCell } from './components/MapCell.js'
4
+
5
+
6
+ // richtext — pure functions only (node classes require 'lexical' context)
7
+ export { fromNan0Html, inventoryNan0Html, FORMAT } from './richtext/fromNan0Html.js'
8
+
9
+ // access control helpers
10
+ export { accessFor, publicAccess } from './access.js'
11
+
12
+
13
+
14
+
15
+
16
+
17
+