@jhits/plugin-content 0.0.5 → 0.0.6

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.
Files changed (37) hide show
  1. package/dist/api/handler.d.ts +18 -0
  2. package/dist/api/handler.d.ts.map +1 -0
  3. package/dist/api/handler.js +57 -0
  4. package/dist/api/router.d.ts +18 -0
  5. package/dist/api/router.d.ts.map +1 -0
  6. package/dist/api/router.js +40 -0
  7. package/dist/api-server.d.ts +7 -0
  8. package/dist/api-server.d.ts.map +1 -0
  9. package/dist/api-server.js +5 -0
  10. package/dist/components/MultilineText.d.ts +48 -0
  11. package/dist/components/MultilineText.d.ts.map +1 -0
  12. package/dist/components/MultilineText.js +39 -0
  13. package/dist/components/ParsedText.d.ts +43 -0
  14. package/dist/components/ParsedText.d.ts.map +1 -0
  15. package/dist/components/ParsedText.js +28 -0
  16. package/dist/components/TranslationEditor.d.ts +2 -0
  17. package/dist/components/TranslationEditor.d.ts.map +1 -0
  18. package/dist/components/TranslationEditor.js +208 -0
  19. package/dist/context/ParserConfigContext.d.ts +31 -0
  20. package/dist/context/ParserConfigContext.d.ts.map +1 -0
  21. package/dist/context/ParserConfigContext.js +32 -0
  22. package/dist/hooks/useParse.d.ts +11 -0
  23. package/dist/hooks/useParse.d.ts.map +1 -0
  24. package/dist/hooks/useParse.js +18 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +32 -0
  28. package/dist/index.server.d.ts +12 -0
  29. package/dist/index.server.d.ts.map +1 -0
  30. package/dist/index.server.js +10 -0
  31. package/dist/utils/parser-config.d.ts +28 -0
  32. package/dist/utils/parser-config.d.ts.map +1 -0
  33. package/dist/utils/parser-config.js +15 -0
  34. package/dist/utils/parser.d.ts +14 -0
  35. package/dist/utils/parser.d.ts.map +1 -0
  36. package/dist/utils/parser.js +64 -0
  37. package/package.json +6 -6
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Content API Handler
3
+ * RESTful API handler for content/translation management
4
+ * Compatible with Next.js API routes
5
+ *
6
+ * IMPORTANT: This file should ONLY be imported in server-side API routes.
7
+ * Do NOT import this in client-side code.
8
+ */
9
+ import { NextRequest, NextResponse } from 'next/server';
10
+ export interface ContentApiConfig {
11
+ /** Directory where locale files are stored (default: 'data/locales') */
12
+ localesDir?: string;
13
+ }
14
+ /**
15
+ * POST /api/plugin-content/save - Save translations
16
+ */
17
+ export declare function POST(req: NextRequest, config: ContentApiConfig): Promise<NextResponse>;
18
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,MAAM,WAAW,gBAAgB;IAC7B,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkD5F"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Content API Handler
3
+ * RESTful API handler for content/translation management
4
+ * Compatible with Next.js API routes
5
+ *
6
+ * IMPORTANT: This file should ONLY be imported in server-side API routes.
7
+ * Do NOT import this in client-side code.
8
+ */
9
+ import { NextResponse } from 'next/server';
10
+ import * as fs from 'fs/promises';
11
+ import * as path from 'path';
12
+ /**
13
+ * POST /api/plugin-content/save - Save translations
14
+ */
15
+ export async function POST(req, config) {
16
+ try {
17
+ const body = await req.json();
18
+ const { locale, messages } = body;
19
+ if (!locale || !messages) {
20
+ return NextResponse.json({ error: 'Missing locale or messages' }, { status: 400 });
21
+ }
22
+ const localesDir = config.localesDir || path.join(process.cwd(), 'data/locales');
23
+ const currentLocaleDir = path.join(localesDir, locale);
24
+ // Ensure locale directory exists
25
+ await fs.mkdir(currentLocaleDir, { recursive: true });
26
+ // Save each namespace as a separate JSON file
27
+ for (const [namespace, content] of Object.entries(messages)) {
28
+ const filePath = path.join(currentLocaleDir, `${namespace}.json`);
29
+ await fs.writeFile(filePath, JSON.stringify(content, null, 2), 'utf8');
30
+ // Ensure other locales have empty files for this namespace (if they don't exist)
31
+ try {
32
+ const otherLocales = await fs.readdir(localesDir);
33
+ for (const other of otherLocales) {
34
+ if (other === locale)
35
+ continue;
36
+ const otherFilePath = path.join(localesDir, other, `${namespace}.json`);
37
+ try {
38
+ await fs.access(otherFilePath);
39
+ }
40
+ catch {
41
+ // File doesn't exist, create empty one
42
+ await fs.writeFile(otherFilePath, JSON.stringify({}, null, 2), 'utf8');
43
+ }
44
+ }
45
+ }
46
+ catch (err) {
47
+ // Ignore errors when reading other locales
48
+ console.warn('[ContentAPI] Could not sync other locales:', err);
49
+ }
50
+ }
51
+ return NextResponse.json({ success: true });
52
+ }
53
+ catch (error) {
54
+ console.error('[ContentAPI] POST error:', error);
55
+ return NextResponse.json({ error: 'Failed to save translations', detail: error.message }, { status: 500 });
56
+ }
57
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Plugin Content API Router
3
+ * Centralized API handler for all content plugin routes
4
+ *
5
+ * This router handles requests to /api/plugin-content/*
6
+ * and routes them to the appropriate handler
7
+ */
8
+ import { NextRequest, NextResponse } from 'next/server';
9
+ export interface ContentApiRouterConfig {
10
+ /** Directory where locale files are stored (default: 'data/locales') */
11
+ localesDir?: string;
12
+ }
13
+ /**
14
+ * Handle content API requests
15
+ * Routes requests to appropriate handlers based on path
16
+ */
17
+ export declare function handleContentApi(req: NextRequest, path: string[], config: ContentApiRouterConfig): Promise<NextResponse>;
18
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD,MAAM,WAAW,sBAAsB;IACnC,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAClC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,sBAAsB,GAC/B,OAAO,CAAC,YAAY,CAAC,CAoCvB"}
@@ -0,0 +1,40 @@
1
+ 'use server';
2
+ /**
3
+ * Plugin Content API Router
4
+ * Centralized API handler for all content plugin routes
5
+ *
6
+ * This router handles requests to /api/plugin-content/*
7
+ * and routes them to the appropriate handler
8
+ */
9
+ import { NextResponse } from 'next/server';
10
+ import { POST as SaveHandler } from './handler';
11
+ /**
12
+ * Handle content API requests
13
+ * Routes requests to appropriate handlers based on path
14
+ */
15
+ export async function handleContentApi(req, path, config) {
16
+ const method = req.method;
17
+ const safePath = Array.isArray(path) ? path : [];
18
+ const route = safePath.length > 0 ? safePath[0] : '';
19
+ console.log(`[ContentApiRouter] method=${method}, path=${JSON.stringify(safePath)}, route=${route}, url=${req.url}`);
20
+ try {
21
+ // Route: /api/plugin-content/save
22
+ if (route === 'save') {
23
+ if (method === 'POST') {
24
+ return await SaveHandler(req, config);
25
+ }
26
+ return NextResponse.json({ error: `Method ${method} not allowed for route: save` }, { status: 405 });
27
+ }
28
+ // Route not found
29
+ return NextResponse.json({ error: `Route not found: ${route || '/'}` }, { status: 404 });
30
+ }
31
+ catch (error) {
32
+ console.error('[ContentApiRouter] Error:', error);
33
+ const status = error.message?.includes('not found') ? 404
34
+ : error.message?.includes('Unauthorized') ? 401
35
+ : error.message?.includes('already') ? 409
36
+ : error.message?.includes('too short') ? 400
37
+ : 500;
38
+ return NextResponse.json({ error: error.message || 'Internal server error' }, { status });
39
+ }
40
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Plugin Content - API Server Entry Point
3
+ * Exports the API router for use in Next.js API routes
4
+ */
5
+ export { handleContentApi } from './api/router';
6
+ export type { ContentApiRouterConfig } from './api/router';
7
+ //# sourceMappingURL=api-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-server.d.ts","sourceRoot":"","sources":["../src/api-server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Plugin Content - API Server Entry Point
3
+ * Exports the API router for use in Next.js API routes
4
+ */
5
+ export { handleContentApi } from './api/router';
@@ -0,0 +1,48 @@
1
+ import { ParserConfig } from '../utils/parser-config';
2
+ export interface MultilineTextProps {
3
+ /**
4
+ * Text content - can be a string (multiline format) or an object (legacy format)
5
+ * String format: Use \n\n for paragraph breaks, \n for line breaks
6
+ * Object format: { "0": "paragraph 1", "1": "paragraph 2" }
7
+ */
8
+ text: string | Record<string, string> | null | undefined;
9
+ /**
10
+ * CSS classes to apply to each paragraph element
11
+ * @default "text-lg text-gray-700 leading-relaxed font-light whitespace-pre-line"
12
+ */
13
+ className?: string;
14
+ /**
15
+ * Wrapper element tag
16
+ * @default "div"
17
+ */
18
+ wrapper?: 'div' | 'span' | 'section';
19
+ /**
20
+ * CSS classes for the wrapper element
21
+ * @default "space-y-6"
22
+ */
23
+ wrapperClassName?: string;
24
+ /**
25
+ * Parser configuration for customizing formatting styles
26
+ * If not provided, uses the default configuration
27
+ */
28
+ config?: ParserConfig;
29
+ }
30
+ /**
31
+ * MultilineText Component
32
+ *
33
+ * Renders text content that can be either:
34
+ * - A multiline string (new format): splits by \n\n for paragraphs, preserves \n for line breaks
35
+ * - An object with keys (legacy format): renders each value as a paragraph
36
+ *
37
+ * Automatically applies formatting via the parse() function and handles whitespace.
38
+ *
39
+ * @example
40
+ * // Multiline string format
41
+ * <MultilineText text="First paragraph\n\nSecond paragraph\nWith line break" />
42
+ *
43
+ * @example
44
+ * // Legacy object format (backward compatible)
45
+ * <MultilineText text={{ "0": "First paragraph", "1": "Second paragraph" }} />
46
+ */
47
+ export default function MultilineText({ text, className, wrapper, wrapperClassName, config: propConfig }: MultilineTextProps): import("react/jsx-runtime").JSX.Element | null;
48
+ //# sourceMappingURL=MultilineText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultilineText.d.ts","sourceRoot":"","sources":["../../src/components/MultilineText.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtD,MAAM,WAAW,kBAAkB;IAC/B;;;;OAIG;IACH,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAEzD;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;IAErC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EAClC,IAAI,EACJ,SAAkF,EAClF,OAAe,EACf,gBAA8B,EAC9B,MAAM,EAAE,UAAU,EACrB,EAAE,kBAAkB,kDAwCpB"}
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { parse } from '../utils/parser';
3
+ import { useParserConfig } from '../context/ParserConfigContext';
4
+ /**
5
+ * MultilineText Component
6
+ *
7
+ * Renders text content that can be either:
8
+ * - A multiline string (new format): splits by \n\n for paragraphs, preserves \n for line breaks
9
+ * - An object with keys (legacy format): renders each value as a paragraph
10
+ *
11
+ * Automatically applies formatting via the parse() function and handles whitespace.
12
+ *
13
+ * @example
14
+ * // Multiline string format
15
+ * <MultilineText text="First paragraph\n\nSecond paragraph\nWith line break" />
16
+ *
17
+ * @example
18
+ * // Legacy object format (backward compatible)
19
+ * <MultilineText text={{ "0": "First paragraph", "1": "Second paragraph" }} />
20
+ */
21
+ export default function MultilineText({ text, className = "text-lg text-gray-700 leading-relaxed font-light whitespace-pre-line", wrapper = 'div', wrapperClassName = "space-y-6", config: propConfig }) {
22
+ if (!text)
23
+ return null;
24
+ // Use prop config if provided, otherwise use context config
25
+ const contextConfig = useParserConfig();
26
+ const config = propConfig || contextConfig;
27
+ const Wrapper = wrapper;
28
+ // Handle string format (new multiline format)
29
+ if (typeof text === 'string') {
30
+ const paragraphs = text.split('\n\n').filter(p => p.trim());
31
+ return (_jsx(Wrapper, { className: wrapperClassName, children: paragraphs.map((paragraph, index) => (_jsx("p", { className: className, children: parse(paragraph, false, config) }, index))) }));
32
+ }
33
+ // Handle object format (legacy format - backward compatibility)
34
+ if (typeof text === 'object' && text !== null) {
35
+ const keys = Object.keys(text);
36
+ return (_jsx(Wrapper, { className: wrapperClassName, children: keys.map((key) => (_jsx("p", { className: className, children: parse(text[key], false, config) }, key))) }));
37
+ }
38
+ return null;
39
+ }
@@ -0,0 +1,43 @@
1
+ import { ParserConfig } from '../utils/parser-config';
2
+ export interface ParsedTextProps {
3
+ /**
4
+ * Text content to parse and render
5
+ */
6
+ text: string | null | undefined;
7
+ /**
8
+ * CSS classes to apply to the wrapper element
9
+ */
10
+ className?: string;
11
+ /**
12
+ * Wrapper element tag
13
+ * @default "span"
14
+ */
15
+ as?: 'span' | 'div' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
16
+ /**
17
+ * Whether to use editor mode (shows formatting markers)
18
+ * @default false
19
+ */
20
+ isEditor?: boolean;
21
+ /**
22
+ * Parser configuration for customizing formatting styles
23
+ * If not provided, uses the default configuration
24
+ */
25
+ config?: ParserConfig;
26
+ }
27
+ /**
28
+ * ParsedText Component
29
+ *
30
+ * Client component that parses and renders text with formatting support.
31
+ * Can be used in server components by passing the text as a prop.
32
+ *
33
+ * Supports:
34
+ * - _*text*_ -> Sage, Italic, Medium
35
+ * - **text** -> Forest, Black (Bold)
36
+ * - /text/ -> Sage (Standard)
37
+ *
38
+ * @example
39
+ * // In a server component
40
+ * <ParsedText text={t('hero.title')} as="h1" className="text-4xl" />
41
+ */
42
+ export default function ParsedText({ text, className, as: Component, isEditor, config: propConfig }: ParsedTextProps): import("react/jsx-runtime").JSX.Element | null;
43
+ //# sourceMappingURL=ParsedText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParsedText.d.ts","sourceRoot":"","sources":["../../src/components/ParsedText.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtD,MAAM,WAAW,eAAe;IAC5B;;OAEG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAEhC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAEpE;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAC/B,IAAI,EACJ,SAAS,EACT,EAAE,EAAE,SAAkB,EACtB,QAAgB,EAChB,MAAM,EAAE,UAAU,EACrB,EAAE,eAAe,kDAUjB"}
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { parse } from '../utils/parser';
4
+ import { useParserConfig } from '../context/ParserConfigContext';
5
+ /**
6
+ * ParsedText Component
7
+ *
8
+ * Client component that parses and renders text with formatting support.
9
+ * Can be used in server components by passing the text as a prop.
10
+ *
11
+ * Supports:
12
+ * - _*text*_ -> Sage, Italic, Medium
13
+ * - **text** -> Forest, Black (Bold)
14
+ * - /text/ -> Sage (Standard)
15
+ *
16
+ * @example
17
+ * // In a server component
18
+ * <ParsedText text={t('hero.title')} as="h1" className="text-4xl" />
19
+ */
20
+ export default function ParsedText({ text, className, as: Component = 'span', isEditor = false, config: propConfig }) {
21
+ if (!text)
22
+ return null;
23
+ // Use prop config if provided, otherwise use context config
24
+ const contextConfig = useParserConfig();
25
+ const config = propConfig || contextConfig;
26
+ const parsed = parse(text, isEditor, config);
27
+ return _jsx(Component, { className: className, children: parsed });
28
+ }
@@ -0,0 +1,2 @@
1
+ export default function TranslationEditor(): import("react/jsx-runtime").JSX.Element | null;
2
+ //# sourceMappingURL=TranslationEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TranslationEditor.d.ts","sourceRoot":"","sources":["../../src/components/TranslationEditor.tsx"],"names":[],"mappings":"AASA,MAAM,CAAC,OAAO,UAAU,iBAAiB,mDAuRxC"}
@@ -0,0 +1,208 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useRouter, usePathname } from 'next/navigation';
4
+ import { useState, useEffect, useMemo, useRef } from 'react';
5
+ import { useLocale, useMessages } from 'next-intl';
6
+ import { motion, AnimatePresence } from 'framer-motion';
7
+ import { X, Globe, Search, ChevronRight, Plus, CornerDownRight, ShieldCheck, Lock, Bold, Italic, Type } from 'lucide-react';
8
+ import { parse } from '../utils/parser';
9
+ export default function TranslationEditor() {
10
+ const router = useRouter();
11
+ const pathname = usePathname();
12
+ const messages = useMessages();
13
+ const locale = useLocale();
14
+ const [open, setOpen] = useState(false);
15
+ const [userData, setUserData] = useState(null);
16
+ const [loadingAuth, setLoadingAuth] = useState(true);
17
+ const [jsonData, setJsonData] = useState(messages);
18
+ const [searchQuery, setSearchQuery] = useState('');
19
+ const [saving, setSaving] = useState(false);
20
+ const [activePath, setActivePath] = useState(null);
21
+ const [showAddForm, setShowAddForm] = useState(false);
22
+ const [newKey, setNewKey] = useState('');
23
+ const [newValue, setNewValue] = useState('');
24
+ // Get all top-level keys for the dropdown (home, about, services, etc.)
25
+ const availableSections = useMemo(() => {
26
+ return Object.keys(jsonData).filter(key => typeof jsonData[key] === 'object');
27
+ }, [jsonData]);
28
+ const [selectedSection, setSelectedSection] = useState('');
29
+ const textareaRefs = useRef({});
30
+ useEffect(() => {
31
+ async function checkAuth() {
32
+ try {
33
+ const res = await fetch('/api/plugin-dep/me');
34
+ const data = await res.json();
35
+ if (data.loggedIn)
36
+ setUserData(data.user);
37
+ }
38
+ catch (err) {
39
+ console.error("Auth check failed", err);
40
+ }
41
+ finally {
42
+ setLoadingAuth(false);
43
+ }
44
+ }
45
+ checkAuth();
46
+ }, []);
47
+ const isAdmin = userData?.role === 'admin' || userData?.role === 'dev';
48
+ const isDev = userData?.role === 'dev';
49
+ const applyFormatting = (path, type) => {
50
+ const el = textareaRefs.current[path];
51
+ if (!el)
52
+ return;
53
+ const start = el.selectionStart;
54
+ const end = el.selectionEnd;
55
+ const text = el.value;
56
+ const selectedText = text.substring(start, end);
57
+ if (!selectedText)
58
+ return;
59
+ let prefix = '', suffix = '';
60
+ if (type === 'bold') {
61
+ prefix = '**';
62
+ suffix = '**';
63
+ }
64
+ if (type === 'italic') {
65
+ prefix = '/';
66
+ suffix = '/';
67
+ }
68
+ if (type === 'both') {
69
+ prefix = '_*';
70
+ suffix = '*_';
71
+ }
72
+ let newText = '';
73
+ if (selectedText.startsWith(prefix) && selectedText.endsWith(suffix)) {
74
+ newText = text.substring(0, start) + selectedText.slice(prefix.length, -suffix.length) + text.substring(end);
75
+ }
76
+ else {
77
+ newText = text.substring(0, start) + prefix + selectedText + suffix + text.substring(end);
78
+ }
79
+ updateJsonData(path, newText);
80
+ setTimeout(() => {
81
+ el.focus();
82
+ el.setSelectionRange(start, start + (newText.length - text.length + selectedText.length));
83
+ }, 0);
84
+ };
85
+ const handleKeyDown = (e, path) => {
86
+ if ((e.metaKey || e.ctrlKey)) {
87
+ if (e.key === 'b') {
88
+ e.preventDefault();
89
+ applyFormatting(path, 'bold');
90
+ }
91
+ if (e.key === 'i') {
92
+ e.preventDefault();
93
+ applyFormatting(path, 'italic');
94
+ }
95
+ if (e.key === 'u') {
96
+ e.preventDefault();
97
+ applyFormatting(path, 'both');
98
+ }
99
+ }
100
+ };
101
+ const updateJsonData = (path, value) => {
102
+ const keys = path.split('.');
103
+ const newData = structuredClone(jsonData);
104
+ let curr = newData;
105
+ for (let i = 0; i < keys.length - 1; i++)
106
+ curr = curr[keys[i]];
107
+ curr[keys[keys.length - 1]] = value;
108
+ setJsonData(newData);
109
+ };
110
+ const handleSave = async () => {
111
+ setSaving(true);
112
+ try {
113
+ const res = await fetch('/api/plugin-content/save', {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify({ locale, messages: jsonData }),
117
+ });
118
+ if (res.ok) {
119
+ router.refresh();
120
+ setShowAddForm(false);
121
+ }
122
+ else {
123
+ alert('Fout bij opslaan');
124
+ }
125
+ }
126
+ catch (err) {
127
+ alert('Netwerkfout bij opslaan');
128
+ }
129
+ finally {
130
+ setSaving(false);
131
+ }
132
+ };
133
+ const addNewEntry = () => {
134
+ if (!isDev || !newKey.includes('.'))
135
+ return;
136
+ updateJsonData(newKey, newValue);
137
+ setNewKey('');
138
+ setNewValue('');
139
+ setShowAddForm(false);
140
+ };
141
+ // Set the initial section based on the URL, but only if it matches a JSON key
142
+ useEffect(() => {
143
+ const parts = pathname.split('/').filter(Boolean);
144
+ const cleanParts = (parts[0]?.length === 2) ? parts.slice(1) : parts;
145
+ const urlKey = cleanParts[0] || 'home';
146
+ // If "over-mij" isn't in JSON, but "about" is, this helps her pick
147
+ if (availableSections.includes(urlKey)) {
148
+ setSelectedSection(urlKey);
149
+ }
150
+ else if (availableSections.length > 0 && !selectedSection) {
151
+ setSelectedSection(availableSections[0]);
152
+ }
153
+ }, [pathname, availableSections]);
154
+ const filteredItems = useMemo(() => {
155
+ const allPaths = getFlattenedPaths(jsonData);
156
+ const filtered = allPaths.filter(item => {
157
+ if (activePath === item.path)
158
+ return true;
159
+ const matchesSearch = item.path.toLowerCase().includes(searchQuery.toLowerCase()) ||
160
+ item.value.toLowerCase().includes(searchQuery.toLowerCase());
161
+ if (searchQuery)
162
+ return matchesSearch;
163
+ // Default view: Selected Section + Common
164
+ return item.path.startsWith(`${selectedSection}.`) || item.path.startsWith('common.');
165
+ });
166
+ return [...filtered].sort((a, b) => {
167
+ const getPriority = (path) => {
168
+ if (path.startsWith(`${selectedSection}.`))
169
+ return 1;
170
+ if (path.startsWith('common.'))
171
+ return 3;
172
+ return 2;
173
+ };
174
+ const priorityA = getPriority(a.path);
175
+ const priorityB = getPriority(b.path);
176
+ return priorityA !== priorityB ? priorityA - priorityB : a.path.localeCompare(b.path);
177
+ });
178
+ }, [jsonData, searchQuery, selectedSection, activePath]);
179
+ if (loadingAuth || !isAdmin)
180
+ return null;
181
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: () => setOpen(true), className: "fixed bottom-6 right-6 bg-neutral-950 dark:bg-white text-white dark:text-neutral-950 px-6 py-3 rounded-full flex items-center gap-3 shadow-2xl z-50 hover:bg-neutral-900 dark:hover:bg-neutral-100 transition-all border border-neutral-800/20 dark:border-neutral-200/20", children: [_jsx(Globe, { size: 18 }), " ", _jsx("span", { className: "font-bold", children: "Editor" })] }), _jsx(AnimatePresence, { children: open && (_jsxs(_Fragment, { children: [_jsx(motion.div, { className: "fixed inset-0 bg-neutral-950/50 dark:bg-neutral-950/70 z-40", initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, onClick: () => setOpen(false) }), _jsxs(motion.div, { className: "fixed right-0 top-0 bottom-0 bg-card text-foreground shadow-2xl z-50 flex flex-col border-l border-border w-[550px]", initial: { x: '100%' }, animate: { x: 0 }, exit: { x: '100%' }, children: [_jsxs("div", { className: "p-6 bg-card border-b border-border flex justify-between items-center", children: [_jsxs("div", { children: [_jsxs("h2", { className: "text-2xl font-serif flex items-center gap-2", children: [isDev ? 'Dev Mode' : 'Content Editor', isDev ? _jsx(ShieldCheck, { className: "text-primary", size: 20 }) : _jsx(Lock, { className: "text-primary", size: 16 })] }), _jsxs("p", { className: "text-[10px] uppercase font-bold text-neutral-500 dark:text-neutral-400", children: ["Welkom, ", userData?.name] })] }), _jsxs("div", { className: "flex gap-2", children: [isDev && (_jsx("button", { onClick: () => setShowAddForm(!showAddForm), className: `p-2 rounded-full transition-colors ${showAddForm ? 'bg-primary text-white' : 'hover:bg-neutral-100 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400'}`, children: _jsx(Plus, { size: 22 }) })), _jsx("button", { onClick: () => setOpen(false), className: "p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-full text-neutral-600 dark:text-neutral-400", children: _jsx(X, { size: 24 }) })] })] }), isDev && showAddForm && (_jsxs(motion.div, { initial: { height: 0 }, animate: { height: 'auto' }, className: "bg-neutral-100/50 dark:bg-neutral-800/50 border-b border-border p-6 space-y-4 overflow-hidden", children: [_jsx("input", { value: newKey, onChange: e => setNewKey(e.target.value), placeholder: "pagina.sectie.sleutel", className: "w-full p-3 rounded-xl border border-border bg-card text-foreground placeholder:text-neutral-500 dark:placeholder:text-neutral-400 text-sm font-mono" }), _jsx("textarea", { value: newValue, onChange: e => setNewValue(e.target.value), placeholder: "Nieuwe vertaling...", className: "w-full p-3 rounded-xl border border-border bg-card text-foreground placeholder:text-neutral-500 dark:placeholder:text-neutral-400 text-sm min-h-[80px]" }), _jsxs("button", { onClick: addNewEntry, className: "w-full py-3 bg-primary text-white rounded-xl font-bold text-sm flex items-center justify-center gap-2", children: [_jsx(CornerDownRight, { size: 16 }), " Toevoegen"] })] })), _jsx("div", { className: "px-6 py-4 bg-card border-b border-border", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500 dark:text-neutral-400" }), _jsx("input", { type: "text", placeholder: "Zoek in teksten...", className: "w-full pl-10 pr-4 py-2.5 bg-neutral-100/50 dark:bg-neutral-800/50 border border-border rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/20 text-foreground placeholder:text-neutral-500 dark:placeholder:text-neutral-400", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) })] }) }), _jsx("div", { className: "flex-1 overflow-y-auto p-6 space-y-10 bg-background pb-32", children: filteredItems.map((item) => (_jsxs("div", { className: "group relative", children: [_jsxs("div", { className: "flex justify-between items-center mb-3", children: [_jsx("div", { className: "flex items-center gap-2", children: item.path.split('.').map((part, i, arr) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider ${i === arr.length - 1 ? 'bg-primary text-white' : 'bg-neutral-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 border border-border'}`, children: part.replace(/_/g, ' ') }), i !== arr.length - 1 && _jsx(ChevronRight, { size: 10, className: "text-neutral-400 dark:text-neutral-500" })] }, i))) }), _jsx(AnimatePresence, { children: activePath === item.path && (_jsxs(motion.div, { initial: { opacity: 0, y: 5 }, animate: { opacity: 1, y: 0 }, className: "formatting-toolbar flex gap-1 bg-card border border-border rounded-lg p-1 shadow-sm", children: [_jsx("button", { type: "button", tabIndex: -1, onClick: () => applyFormatting(item.path, 'bold'), className: "p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded text-foreground", children: _jsx(Bold, { size: 14 }) }), _jsx("button", { type: "button", tabIndex: -1, onClick: () => applyFormatting(item.path, 'italic'), className: "p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded text-foreground", children: _jsx(Italic, { size: 14 }) }), _jsx("button", { type: "button", tabIndex: -1, onClick: () => applyFormatting(item.path, 'both'), className: "p-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded text-foreground", children: _jsx(Type, { size: 14 }) })] })) })] }), _jsxs("div", { className: "relative w-full min-h-[60px]", children: [_jsx("div", { className: "absolute inset-0 p-5 text-lg leading-relaxed pointer-events-none whitespace-pre-wrap break-words border-2 border-transparent text-foreground", "aria-hidden": "true", children: parse(item.value, true) }), _jsx("textarea", { ref: (el) => { textareaRefs.current[item.path] = el; }, className: `
182
+ w-full p-5 bg-transparent border-2 rounded-2xl
183
+ text-transparent caret-primary leading-relaxed shadow-sm
184
+ transition-all outline-none resize-none overflow-hidden text-lg
185
+ relative z-10 whitespace-pre-wrap break-words
186
+ ${activePath === item.path ? 'border-primary ring-4 ring-primary/10' : 'border-border'}
187
+ `, value: item.value, rows: 1, spellCheck: false, onFocus: () => setActivePath(item.path), onBlur: (e) => {
188
+ if (!e.relatedTarget?.closest('.formatting-toolbar')) {
189
+ setActivePath(null);
190
+ }
191
+ }, onKeyDown: (e) => handleKeyDown(e, item.path), onChange: (e) => updateJsonData(item.path, e.target.value), onInput: (e) => {
192
+ e.target.style.height = 'auto';
193
+ e.target.style.height = e.target.scrollHeight + 'px';
194
+ } })] })] }, item.path))) }), _jsx("div", { className: "p-6 bg-card border-t border-border flex justify-end", children: _jsx("button", { onClick: handleSave, disabled: saving, className: "bg-primary text-white px-12 py-3.5 rounded-full font-bold shadow-lg disabled:opacity-50 transition-all hover:scale-105 active:scale-95", children: saving ? 'Bezig...' : 'Wijzigingen Opslaan' }) })] })] })) })] }));
195
+ }
196
+ function getFlattenedPaths(obj, prefix = '') {
197
+ let paths = [];
198
+ for (const key in obj) {
199
+ const fullPath = prefix ? `${prefix}.${key}` : key;
200
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
201
+ paths = [...paths, ...getFlattenedPaths(obj[key], fullPath)];
202
+ }
203
+ else {
204
+ paths.push({ path: fullPath, value: String(obj[key]) });
205
+ }
206
+ }
207
+ return paths;
208
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { ParserConfig } from '../utils/parser-config';
3
+ export interface ParserConfigProviderProps {
4
+ /**
5
+ * Parser configuration to use globally
6
+ * If not provided, uses the default configuration
7
+ */
8
+ config?: ParserConfig;
9
+ children: React.ReactNode;
10
+ }
11
+ /**
12
+ * ParserConfigProvider
13
+ *
14
+ * Provides a global parser configuration to all child components.
15
+ * Components can override this with their own config prop.
16
+ *
17
+ * @example
18
+ * <ParserConfigProvider config={{
19
+ * bold: { className: "font-bold text-blue-600" },
20
+ * italic: { className: "italic text-gray-500" }
21
+ * }}>
22
+ * <App />
23
+ * </ParserConfigProvider>
24
+ */
25
+ export declare function ParserConfigProvider({ config, children }: ParserConfigProviderProps): import("react/jsx-runtime").JSX.Element;
26
+ /**
27
+ * Hook to get the current parser configuration from context
28
+ * Falls back to defaultParserConfig if no provider is found
29
+ */
30
+ export declare function useParserConfig(): ParserConfig;
31
+ //# sourceMappingURL=ParserConfigContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParserConfigContext.d.ts","sourceRoot":"","sources":["../../src/context/ParserConfigContext.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoC,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,YAAY,EAAuB,MAAM,wBAAwB,CAAC;AAU3E,MAAM,WAAW,yBAAyB;IACtC;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,MAA4B,EAAE,QAAQ,EAAE,EAAE,yBAAyB,2CAMzG;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAG9C"}
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from 'react';
4
+ import { defaultParserConfig } from '../utils/parser-config';
5
+ const ParserConfigContext = createContext({
6
+ config: defaultParserConfig
7
+ });
8
+ /**
9
+ * ParserConfigProvider
10
+ *
11
+ * Provides a global parser configuration to all child components.
12
+ * Components can override this with their own config prop.
13
+ *
14
+ * @example
15
+ * <ParserConfigProvider config={{
16
+ * bold: { className: "font-bold text-blue-600" },
17
+ * italic: { className: "italic text-gray-500" }
18
+ * }}>
19
+ * <App />
20
+ * </ParserConfigProvider>
21
+ */
22
+ export function ParserConfigProvider({ config = defaultParserConfig, children }) {
23
+ return (_jsx(ParserConfigContext.Provider, { value: { config }, children: children }));
24
+ }
25
+ /**
26
+ * Hook to get the current parser configuration from context
27
+ * Falls back to defaultParserConfig if no provider is found
28
+ */
29
+ export function useParserConfig() {
30
+ const { config } = useContext(ParserConfigContext);
31
+ return config;
32
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * Hook that returns a configured parse function
4
+ * Uses the parser configuration from context
5
+ *
6
+ * @example
7
+ * const parse = useParse();
8
+ * const formatted = parse('This is **bold** text');
9
+ */
10
+ export declare function useParse(): (text: string, isEditor?: boolean) => ReactNode;
11
+ //# sourceMappingURL=useParse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useParse.d.ts","sourceRoot":"","sources":["../../src/hooks/useParse.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;GAOG;AACH,wBAAgB,QAAQ,WAGM,MAAM,yBAAqB,SAAS,CAGjE"}
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+ import { useCallback } from 'react';
3
+ import { parse as parseFunction } from '../utils/parser';
4
+ import { useParserConfig } from '../context/ParserConfigContext';
5
+ /**
6
+ * Hook that returns a configured parse function
7
+ * Uses the parser configuration from context
8
+ *
9
+ * @example
10
+ * const parse = useParse();
11
+ * const formatted = parse('This is **bold** text');
12
+ */
13
+ export function useParse() {
14
+ const config = useParserConfig();
15
+ return useCallback((text, isEditor = false) => {
16
+ return parseFunction(text, isEditor, config);
17
+ }, [config]);
18
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Plugin Content - Main Entry Point
3
+ * Provides content editing functionality for website translations
4
+ */
5
+ export interface ContentPluginProps {
6
+ /** Whether to show the editor (default: true) */
7
+ enabled?: boolean;
8
+ }
9
+ /**
10
+ * Content Plugin Component
11
+ * Renders the translation editor for editing website content
12
+ */
13
+ declare function ContentPlugin({ enabled }: ContentPluginProps): import("react/jsx-runtime").JSX.Element | null;
14
+ export default ContentPlugin;
15
+ export { ContentPlugin };
16
+ export { default as TranslationEditor } from './components/TranslationEditor';
17
+ export { default as MultilineText } from './components/MultilineText';
18
+ export type { MultilineTextProps } from './components/MultilineText';
19
+ export { default as ParsedText } from './components/ParsedText';
20
+ export type { ParsedTextProps } from './components/ParsedText';
21
+ export { ParserConfigProvider, useParserConfig } from './context/ParserConfigContext';
22
+ export type { ParserConfigProviderProps } from './context/ParserConfigContext';
23
+ export { useParse } from './hooks/useParse';
24
+ export { parse } from './utils/parser';
25
+ export { defaultParserConfig } from './utils/parser-config';
26
+ export type { ParserConfig, FormatStyle } from './utils/parser-config';
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,kBAAkB;IAC/B,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,iBAAS,aAAa,CAAC,EAAE,OAAc,EAAE,EAAE,kBAAkB,kDAI5D;AAGD,eAAe,aAAa,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,CAAC;AAGzB,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG/D,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACtF,YAAY,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAG/E,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI5C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Plugin Content - Main Entry Point
3
+ * Provides content editing functionality for website translations
4
+ */
5
+ 'use client';
6
+ import { jsx as _jsx } from "react/jsx-runtime";
7
+ import TranslationEditor from './components/TranslationEditor';
8
+ /**
9
+ * Content Plugin Component
10
+ * Renders the translation editor for editing website content
11
+ */
12
+ function ContentPlugin({ enabled = true }) {
13
+ if (!enabled)
14
+ return null;
15
+ return _jsx(TranslationEditor, {});
16
+ }
17
+ // Export as both default and named export for flexibility
18
+ export default ContentPlugin;
19
+ export { ContentPlugin };
20
+ // Export components
21
+ export { default as TranslationEditor } from './components/TranslationEditor';
22
+ export { default as MultilineText } from './components/MultilineText';
23
+ export { default as ParsedText } from './components/ParsedText';
24
+ // Export context
25
+ export { ParserConfigProvider, useParserConfig } from './context/ParserConfigContext';
26
+ // Export hooks
27
+ export { useParse } from './hooks/useParse';
28
+ // Export utilities
29
+ // Note: parse() is a client-only function. Use ParsedText component in server components.
30
+ export { parse } from './utils/parser';
31
+ // Export parser configuration
32
+ export { defaultParserConfig } from './utils/parser-config';
@@ -0,0 +1,12 @@
1
+ import 'server-only';
2
+ /**
3
+ * Plugin Content - Server-Only Entry Point
4
+ * This file exports only server-side API handlers
5
+ * Used by the dynamic plugin router via @jhits/plugin-content/server
6
+ *
7
+ * Note: This file is server-only (no 'use server' needed - that's only for Server Actions)
8
+ */
9
+ export { handleContentApi as handleApi } from './api/router';
10
+ export { handleContentApi } from './api/router';
11
+ export type { ContentApiRouterConfig } from './api/router';
12
+ //# sourceMappingURL=index.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.server.d.ts","sourceRoot":"","sources":["../src/index.server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB;;;;;;GAMG;AAEH,OAAO,EAAE,gBAAgB,IAAI,SAAS,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,10 @@
1
+ import 'server-only';
2
+ /**
3
+ * Plugin Content - Server-Only Entry Point
4
+ * This file exports only server-side API handlers
5
+ * Used by the dynamic plugin router via @jhits/plugin-content/server
6
+ *
7
+ * Note: This file is server-only (no 'use server' needed - that's only for Server Actions)
8
+ */
9
+ export { handleContentApi as handleApi } from './api/router';
10
+ export { handleContentApi } from './api/router'; // Keep original export for backward compatibility
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ /**
3
+ * Style configuration for a formatting marker
4
+ */
5
+ export interface FormatStyle {
6
+ /** CSS classes to apply to the formatted text */
7
+ className?: string;
8
+ /** Inline styles to apply */
9
+ style?: React.CSSProperties;
10
+ }
11
+ /**
12
+ * Parser configuration
13
+ * Defines how different formatting markers are styled
14
+ */
15
+ export interface ParserConfig {
16
+ /** Style for _*text*_ (sage italic medium) */
17
+ sageItalic?: FormatStyle;
18
+ /** Style for **text** (bold forest) */
19
+ bold?: FormatStyle;
20
+ /** Style for /text/ (standard sage) */
21
+ italic?: FormatStyle;
22
+ }
23
+ /**
24
+ * Default parser configuration
25
+ * Matches the original styling from the client app
26
+ */
27
+ export declare const defaultParserConfig: ParserConfig;
28
+ //# sourceMappingURL=parser-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser-config.d.ts","sourceRoot":"","sources":["../../src/utils/parser-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IACzB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,uCAAuC;IACvC,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,uCAAuC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,YAUjC,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Default parser configuration
3
+ * Matches the original styling from the client app
4
+ */
5
+ export const defaultParserConfig = {
6
+ sageItalic: {
7
+ className: "text-sage italic font-medium brightness-130"
8
+ },
9
+ bold: {
10
+ className: "text-forest font-black"
11
+ },
12
+ italic: {
13
+ className: "text-sage"
14
+ }
15
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { ParserConfig } from './parser-config';
3
+ /**
4
+ * Parses text with configurable formatting:
5
+ * _*text*_ -> Sage Italic (configurable via config.sageItalic)
6
+ * **text** -> Bold (configurable via config.bold)
7
+ * /text/ -> Italic (configurable via config.italic)
8
+ *
9
+ * @param text - The text to parse
10
+ * @param isEditor - Whether to show formatting markers (for editor mode)
11
+ * @param config - Optional parser configuration. Uses defaultParserConfig if not provided.
12
+ */
13
+ export declare function parse(text: string, isEditor?: boolean, config?: ParserConfig): React.ReactNode;
14
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/utils/parser.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAuB,MAAM,iBAAiB,CAAC;AAEpE;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CACjB,IAAI,EAAE,MAAM,EACZ,QAAQ,UAAQ,EAChB,MAAM,GAAE,YAAkC,GAC3C,KAAK,CAAC,SAAS,CAyGjB"}
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { defaultParserConfig } from './parser-config';
4
+ /**
5
+ * Parses text with configurable formatting:
6
+ * _*text*_ -> Sage Italic (configurable via config.sageItalic)
7
+ * **text** -> Bold (configurable via config.bold)
8
+ * /text/ -> Italic (configurable via config.italic)
9
+ *
10
+ * @param text - The text to parse
11
+ * @param isEditor - Whether to show formatting markers (for editor mode)
12
+ * @param config - Optional parser configuration. Uses defaultParserConfig if not provided.
13
+ */
14
+ export function parse(text, isEditor = false, config = defaultParserConfig) {
15
+ if (!text)
16
+ return null;
17
+ // Merge with default config to ensure all styles are defined
18
+ const finalConfig = {
19
+ sageItalic: { ...defaultParserConfig.sageItalic, ...config.sageItalic },
20
+ bold: { ...defaultParserConfig.bold, ...config.bold },
21
+ italic: { ...defaultParserConfig.italic, ...config.italic }
22
+ };
23
+ // The Regex must capture ALL your markers, otherwise they won't reach the 'if' statements
24
+ // We look for: _*...*_ OR **...** OR /.../
25
+ const parts = text.split(/(_\*.*?\*\_|\*\*.*?\*\*|\/.*?\/)/g);
26
+ const renderFormatted = (part, index) => {
27
+ // 1. Sage Italic: _*text*_ (Check first because it's the most specific)
28
+ if (part.startsWith('_*') && part.endsWith('*_')) {
29
+ const inner = part.slice(2, -2);
30
+ const style = finalConfig.sageItalic;
31
+ return (_jsx("span", { className: style.className, style: style.style, children: inner }, index));
32
+ }
33
+ // 2. Bold: **text**
34
+ if (part.startsWith('**') && part.endsWith('**')) {
35
+ const inner = part.slice(2, -2);
36
+ const style = finalConfig.bold;
37
+ return (_jsx("span", { className: style.className, style: style.style, children: inner }, index));
38
+ }
39
+ // 3. Italic: /text/
40
+ if (part.startsWith('/') && part.endsWith('/')) {
41
+ const inner = part.slice(1, -1);
42
+ const style = finalConfig.italic;
43
+ return (_jsx("span", { className: style.className, style: style.style, children: inner }, index));
44
+ }
45
+ // 4. Plain text
46
+ return _jsx(React.Fragment, { children: part }, index);
47
+ };
48
+ return !isEditor ? parts.map(renderFormatted) : parts.map((part, index) => {
49
+ const ghost = (sym) => isEditor ? _jsx("span", { className: "opacity-30 font-mono text-[0.8em]", children: sym }) : null;
50
+ if (part.startsWith('_*') && part.endsWith('*_')) {
51
+ const style = finalConfig.sageItalic;
52
+ return (_jsxs("span", { className: style.className, style: style.style, children: [ghost('_*'), part.slice(2, -2), ghost('*_')] }, index));
53
+ }
54
+ if (part.startsWith('**') && part.endsWith('**')) {
55
+ const style = finalConfig.bold;
56
+ return (_jsxs("span", { className: style.className, style: style.style, children: [ghost('**'), part.slice(2, -2), ghost('**')] }, index));
57
+ }
58
+ if (part.startsWith('/') && part.endsWith('/')) {
59
+ const style = finalConfig.italic;
60
+ return (_jsxs("span", { className: style.className, style: style.style, children: [ghost('/'), part.slice(1, -1), ghost('/')] }, index));
61
+ }
62
+ return _jsx(React.Fragment, { children: part }, index);
63
+ });
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhits/plugin-content",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Content management and localization plugin for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -17,6 +17,9 @@
17
17
  "default": "./dist/index.server.js"
18
18
  }
19
19
  },
20
+ "scripts": {
21
+ "build": "tsc"
22
+ },
20
23
  "dependencies": {
21
24
  "@jhits/plugin-core": "^0.0.2",
22
25
  "framer-motion": "^12.34.0",
@@ -44,8 +47,5 @@
44
47
  "dist",
45
48
  "src",
46
49
  "package.json"
47
- ],
48
- "scripts": {
49
- "build": "tsc"
50
- }
51
- }
50
+ ]
51
+ }