@jhits/plugin-blog 0.0.16 → 0.0.18

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 (67) hide show
  1. package/dist/api/categories.d.ts.map +1 -1
  2. package/dist/api/categories.js +43 -12
  3. package/dist/api/handler.d.ts +1 -0
  4. package/dist/api/handler.d.ts.map +1 -1
  5. package/dist/api/handler.js +259 -32
  6. package/dist/hooks/useBlogs.d.ts +2 -0
  7. package/dist/hooks/useBlogs.d.ts.map +1 -1
  8. package/dist/hooks/useBlogs.js +10 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -3
  12. package/dist/lib/i18n.d.ts +14 -0
  13. package/dist/lib/i18n.d.ts.map +1 -0
  14. package/dist/lib/i18n.js +58 -0
  15. package/dist/lib/mappers/apiMapper.d.ts +18 -0
  16. package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
  17. package/dist/lib/mappers/apiMapper.js +1 -0
  18. package/dist/state/reducer.d.ts.map +1 -1
  19. package/dist/state/reducer.js +11 -6
  20. package/dist/state/types.d.ts +5 -0
  21. package/dist/state/types.d.ts.map +1 -1
  22. package/dist/state/types.js +1 -0
  23. package/dist/types/post.d.ts +25 -0
  24. package/dist/types/post.d.ts.map +1 -1
  25. package/dist/utils/client.d.ts +2 -0
  26. package/dist/utils/client.d.ts.map +1 -1
  27. package/dist/utils/client.js +3 -1
  28. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  29. package/dist/views/CanvasEditor/CanvasEditorView.js +130 -4
  30. package/dist/views/CanvasEditor/EditorHeader.d.ts +5 -1
  31. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
  32. package/dist/views/CanvasEditor/EditorHeader.js +23 -5
  33. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +1 -1
  34. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/hooks/usePostLoader.js +14 -4
  36. package/dist/views/PostManager/LanguageFlags.d.ts +11 -0
  37. package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -0
  38. package/dist/views/PostManager/LanguageFlags.js +60 -0
  39. package/dist/views/PostManager/PostCards.d.ts.map +1 -1
  40. package/dist/views/PostManager/PostCards.js +4 -1
  41. package/dist/views/PostManager/PostFilters.d.ts +4 -1
  42. package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
  43. package/dist/views/PostManager/PostFilters.js +13 -3
  44. package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
  45. package/dist/views/PostManager/PostManagerView.js +24 -3
  46. package/dist/views/PostManager/PostTable.d.ts.map +1 -1
  47. package/dist/views/PostManager/PostTable.js +4 -1
  48. package/package.json +4 -4
  49. package/src/api/categories.ts +58 -11
  50. package/src/api/handler.ts +286 -31
  51. package/src/hooks/useBlogs.ts +12 -1
  52. package/src/index.tsx +7 -3
  53. package/src/lib/i18n.ts +78 -0
  54. package/src/lib/mappers/apiMapper.ts +20 -0
  55. package/src/state/reducer.ts +12 -6
  56. package/src/state/types.ts +5 -0
  57. package/src/types/post.ts +28 -0
  58. package/src/utils/client.ts +4 -0
  59. package/src/views/CanvasEditor/CanvasEditorView.tsx +164 -20
  60. package/src/views/CanvasEditor/EditorHeader.tsx +93 -18
  61. package/src/views/CanvasEditor/hooks/usePostLoader.ts +15 -4
  62. package/src/views/PostManager/LanguageFlags.tsx +136 -0
  63. package/src/views/PostManager/PostCards.tsx +22 -12
  64. package/src/views/PostManager/PostFilters.tsx +38 -1
  65. package/src/views/PostManager/PostManagerView.tsx +25 -2
  66. package/src/views/PostManager/PostTable.tsx +12 -1
  67. package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +0 -81
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Language Flags Component
3
+ * Displays interactive language flags with custom tooltips
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React, { useState, useRef, useEffect } from 'react';
9
+ import { createPortal } from 'react-dom';
10
+ import { motion, AnimatePresence } from 'framer-motion';
11
+ import { PostListItem } from '../../types/post';
12
+
13
+ interface LanguageFlagsProps {
14
+ post: PostListItem;
15
+ }
16
+
17
+ const LANGUAGE_COUNTRY_CODES: Record<string, string> = {
18
+ en: 'gb',
19
+ nl: 'nl',
20
+ sv: 'se',
21
+ de: 'de',
22
+ fr: 'fr',
23
+ es: 'es',
24
+ it: 'it',
25
+ pt: 'pt',
26
+ pl: 'pl',
27
+ ru: 'ru',
28
+ ja: 'jp',
29
+ zh: 'cn',
30
+ ar: 'sa',
31
+ tr: 'tr',
32
+ };
33
+
34
+ const getFlagUrl = (lang: string) => {
35
+ const countryCode = LANGUAGE_COUNTRY_CODES[lang] || lang;
36
+ return `https://flagcdn.com/w40/${countryCode}.png`;
37
+ };
38
+
39
+ export function LanguageFlags({ post }: LanguageFlagsProps) {
40
+ const [hoveredLang, setHoveredLang] = useState<string | null>(null);
41
+ const [tooltipCoords, setTooltipCoords] = useState<{ top: number; left: number } | null>(null);
42
+ const flagRefs = useRef<Record<string, HTMLDivElement | null>>({});
43
+
44
+ useEffect(() => {
45
+ if (hoveredLang && flagRefs.current[hoveredLang]) {
46
+ const rect = flagRefs.current[hoveredLang]!.getBoundingClientRect();
47
+ setTooltipCoords({
48
+ top: rect.top, // Viewport-relative
49
+ left: rect.left + rect.width / 2, // Center horizontally
50
+ });
51
+ }
52
+ }, [hoveredLang]);
53
+
54
+ if (!post.availableLanguages || post.availableLanguages.length === 0) {
55
+ return null;
56
+ }
57
+
58
+ return (
59
+ <div className="flex gap-2">
60
+ {post.availableLanguages.map((lang) => {
61
+ const langData = post.languages?.[lang];
62
+ const status = langData?.status || 'draft';
63
+ const langTitle = langData?.metadata?.title || post.title;
64
+ const isHovered = hoveredLang === lang;
65
+
66
+ return (
67
+ <div
68
+ key={lang}
69
+ ref={(el) => { flagRefs.current[lang] = el; }}
70
+ className="relative"
71
+ onMouseEnter={() => setHoveredLang(lang)}
72
+ onMouseLeave={() => setHoveredLang(null)}
73
+ >
74
+ <motion.div
75
+ whileHover={{ scale: 1.15 }}
76
+ className={`relative flex items-center justify-center w-7 h-4.5 rounded shadow-sm overflow-hidden border cursor-help transition-colors ${
77
+ status === 'published'
78
+ ? 'border-green-500/40 bg-white'
79
+ : 'border-amber-500/40 bg-white opacity-90'
80
+ }`}
81
+ >
82
+ <img
83
+ src={getFlagUrl(lang)}
84
+ alt={lang}
85
+ className="w-full h-full object-cover"
86
+ />
87
+
88
+ {status !== 'published' && (
89
+ <div className="absolute inset-0 bg-amber-500/5" />
90
+ )}
91
+ </motion.div>
92
+
93
+ {typeof document !== 'undefined' && createPortal(
94
+ <AnimatePresence>
95
+ {isHovered && tooltipCoords && (
96
+ <motion.div
97
+ initial={{ opacity: 0, y: -90, x: '-50%', scale: 0.95 }}
98
+ animate={{ opacity: 1, y: -100, x: '-50%', scale: 1 }}
99
+ exit={{ opacity: 0, y: -90, x: '-50%', scale: 0.95 }}
100
+ className="fixed z-[9999] pointer-events-none"
101
+ style={{
102
+ top: tooltipCoords.top - 12,
103
+ left: tooltipCoords.left,
104
+ }}
105
+ >
106
+ <div className="bg-neutral-900 dark:bg-neutral-800 text-white p-3 rounded-xl shadow-2xl border border-neutral-700/50 backdrop-blur-md min-w-[180px] max-w-[280px]">
107
+ <div className="flex items-center justify-between gap-4 mb-1.5">
108
+ <span className="text-[10px] font-black uppercase tracking-widest text-neutral-400">
109
+ {lang.toUpperCase()} Version
110
+ </span>
111
+ <span className={`text-[9px] font-black uppercase px-1.5 py-0.5 rounded ${
112
+ status === 'published'
113
+ ? 'bg-green-500/20 text-green-400'
114
+ : 'bg-amber-500/20 text-amber-400'
115
+ }`}>
116
+ {status}
117
+ </span>
118
+ </div>
119
+ <p className="text-xs font-bold leading-snug line-clamp-2">
120
+ {langTitle}
121
+ </p>
122
+
123
+ {/* Tooltip Arrow */}
124
+ <div className="absolute top-full left-1/2 -translate-x-1/2 border-8 border-transparent border-t-neutral-900 dark:border-t-neutral-800" />
125
+ </div>
126
+ </motion.div>
127
+ )}
128
+ </AnimatePresence>,
129
+ document.body
130
+ )}
131
+ </div>
132
+ );
133
+ })}
134
+ </div>
135
+ );
136
+ }
@@ -10,6 +10,7 @@ import { Calendar, User, UserCheck } from 'lucide-react';
10
10
  import { Image } from '@jhits/plugin-images';
11
11
  import { PostListItem, PostStatus } from '../../types/post';
12
12
  import { PostActionsMenu } from './PostActionsMenu';
13
+ import { LanguageFlags } from './LanguageFlags';
13
14
  import { useSession } from 'next-auth/react';
14
15
 
15
16
  export interface PostCardsProps {
@@ -21,7 +22,7 @@ export interface PostCardsProps {
21
22
  onDelete: (postId: string) => void;
22
23
  }
23
24
 
24
- function getStatusBadgeColor(status: PostStatus) {
25
+ function getStatusBadgeColor(status: PostStatus | 'not-translated') {
25
26
  switch (status) {
26
27
  case 'published':
27
28
  return 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20';
@@ -31,6 +32,8 @@ function getStatusBadgeColor(status: PostStatus) {
31
32
  return 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20';
32
33
  case 'archived':
33
34
  return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
35
+ case 'not-translated':
36
+ return 'bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/20 italic';
34
37
  default:
35
38
  return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
36
39
  }
@@ -98,10 +101,10 @@ export function PostCards({
98
101
  {posts.map((post) => (
99
102
  <div
100
103
  key={post.id}
101
- className="bg-dashboard-card rounded-2xl border border-dashboard-border overflow-hidden hover:shadow-xl transition-all duration-300 group"
104
+ className="flex flex-col bg-dashboard-card rounded-2xl border border-dashboard-border overflow-hidden hover:shadow-xl transition-all duration-300 group h-full"
102
105
  >
103
106
  {/* Featured Image */}
104
- <div className="relative w-full h-48 bg-neutral-200 dark:bg-neutral-800 overflow-hidden">
107
+ <div className="relative w-full h-48 bg-neutral-200 dark:bg-neutral-800 overflow-hidden flex-shrink-0">
105
108
  {post.featuredImage ? (
106
109
  <Image
107
110
  id={post.featuredImage}
@@ -139,9 +142,9 @@ export function PostCards({
139
142
  </div>
140
143
 
141
144
  {/* Card Content */}
142
- <div className="p-6">
145
+ <div className="p-6 flex flex-col flex-1">
143
146
  {/* Title & Slug */}
144
- <div className="mb-4">
147
+ <div className="mb-4 min-h-[72px]">
145
148
  <button
146
149
  onClick={() => onEdit(post.id)}
147
150
  className="text-left w-full hover:cursor-pointer"
@@ -150,20 +153,27 @@ export function PostCards({
150
153
  {post.title}
151
154
  </h3>
152
155
  </button>
153
- <p className="text-xs text-neutral-500 dark:text-neutral-400 font-mono">
156
+ <p className="text-xs text-neutral-500 dark:text-neutral-400 font-mono line-clamp-1">
154
157
  /{post.slug}
155
158
  </p>
156
159
  </div>
157
160
 
158
161
  {/* Excerpt */}
159
- {post.excerpt && (
160
- <p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4 line-clamp-2">
161
- {post.excerpt}
162
- </p>
163
- )}
162
+ <div className="flex-1 mb-4 min-h-[40px]">
163
+ {post.excerpt && (
164
+ <p className="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-2">
165
+ {post.excerpt}
166
+ </p>
167
+ )}
168
+ </div>
169
+
170
+ {/* Languages */}
171
+ <div className="mb-6">
172
+ <LanguageFlags post={post} />
173
+ </div>
164
174
 
165
175
  {/* Meta Information */}
166
- <div className="space-y-3 pt-4 border-t border-neutral-200 dark:border-neutral-700">
176
+ <div className="space-y-3 pt-4 border-t border-neutral-200 dark:border-neutral-700 mt-auto">
167
177
  {/* Author */}
168
178
  <div className="flex items-center gap-2">
169
179
  {isPostOwner(post) ? (
@@ -6,7 +6,7 @@
6
6
  'use client';
7
7
 
8
8
  import React from 'react';
9
- import { Search, Filter, Tag } from 'lucide-react';
9
+ import { Search, Filter, Tag, Globe } from 'lucide-react';
10
10
  import { PostStatus } from '../../types/post';
11
11
 
12
12
  export interface PostFiltersProps {
@@ -17,6 +17,9 @@ export interface PostFiltersProps {
17
17
  categoryFilter: string;
18
18
  onCategoryFilterChange: (value: string) => void;
19
19
  categories: string[];
20
+ language: string;
21
+ onLanguageChange: (value: string) => void;
22
+ availableLanguages: string[];
20
23
  }
21
24
 
22
25
  export function PostFilters({
@@ -27,7 +30,20 @@ export function PostFilters({
27
30
  categoryFilter,
28
31
  onCategoryFilterChange,
29
32
  categories,
33
+ language,
34
+ onLanguageChange,
35
+ availableLanguages,
30
36
  }: PostFiltersProps) {
37
+ const langNames: Record<string, string> = {
38
+ nl: 'Nederlands (NL)',
39
+ en: 'English (EN)',
40
+ sv: 'Svenska (SV)',
41
+ de: 'Deutsch (DE)',
42
+ fr: 'Français (FR)',
43
+ es: 'Español (ES)',
44
+ it: 'Italiano (IT)',
45
+ pt: 'Português (PT)',
46
+ };
31
47
  return (
32
48
  <div className="flex flex-col sm:flex-row gap-4 mb-6">
33
49
  {/* Search Input */}
@@ -89,6 +105,27 @@ export function PostFilters({
89
105
  ))}
90
106
  </select>
91
107
  </div>
108
+
109
+ {/* Language Selector */}
110
+ <div className="relative">
111
+ <label htmlFor="blog-post-language-filter" className="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0" style={{ clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }}>
112
+ Primary Language
113
+ </label>
114
+ <Globe className="absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4 pointer-events-none" />
115
+ <select
116
+ id="blog-post-language-filter"
117
+ name="blog-post-language-filter"
118
+ value={language}
119
+ onChange={(e) => onLanguageChange(e.target.value)}
120
+ className="pl-11 pr-8 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none outline-none cursor-pointer min-w-[140px]"
121
+ >
122
+ {availableLanguages.map(lang => (
123
+ <option key={lang} value={lang}>
124
+ {langNames[lang] || lang.toUpperCase()}
125
+ </option>
126
+ ))}
127
+ </select>
128
+ </div>
92
129
  </div>
93
130
  );
94
131
  }
@@ -32,6 +32,7 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
32
32
  const [search, setSearch] = useState('');
33
33
  const [statusFilter, setStatusFilter] = useState<PostStatus | 'all'>('all');
34
34
  const [categoryFilter, setCategoryFilter] = useState<string>('all');
35
+ const [currentLanguage, setCurrentLanguage] = useState<string>(locale || 'nl');
35
36
 
36
37
  // Load view mode preference from localStorage
37
38
  const getStoredViewMode = (): ViewMode => {
@@ -54,7 +55,7 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
54
55
  const fetchPosts = async () => {
55
56
  try {
56
57
  setLoading(true);
57
- const response = await fetch('/api/plugin-blog?admin=true');
58
+ const response = await fetch(`/api/plugin-blog?admin=true&language=${currentLanguage}`);
58
59
  const data = await response.json();
59
60
 
60
61
  if (data.blogs && Array.isArray(data.blogs)) {
@@ -89,6 +90,9 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
89
90
  authorId: blogPost.publication.authorId,
90
91
  updatedAt: blogPost.updatedAt,
91
92
  category: category,
93
+ lang: blogPost.metadata.lang,
94
+ availableLanguages: doc.availableLanguages,
95
+ languages: doc.languages,
92
96
  };
93
97
  });
94
98
  setPosts(postListItems);
@@ -102,7 +106,7 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
102
106
  };
103
107
 
104
108
  fetchPosts();
105
- }, []);
109
+ }, [currentLanguage]);
106
110
 
107
111
  // Extract unique categories from posts
108
112
  const categories = React.useMemo(() => {
@@ -115,6 +119,22 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
115
119
  return Array.from(categorySet).sort();
116
120
  }, [posts]);
117
121
 
122
+ // Extract unique languages from all posts
123
+ const availableLanguages = React.useMemo(() => {
124
+ const langSet = new Set<string>();
125
+ posts.forEach(post => {
126
+ if (post.availableLanguages) {
127
+ post.availableLanguages.forEach(lang => langSet.add(lang));
128
+ }
129
+ if (post.lang) langSet.add(post.lang);
130
+ });
131
+ // Always include the current locale/language to ensure it's selectable
132
+ if (locale) langSet.add(locale);
133
+ if (currentLanguage) langSet.add(currentLanguage);
134
+
135
+ return Array.from(langSet).sort();
136
+ }, [posts, locale, currentLanguage]);
137
+
118
138
  // Filter posts
119
139
  const filteredPosts = React.useMemo(() => {
120
140
  return posts.filter((post) => {
@@ -231,6 +251,9 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
231
251
  categoryFilter={categoryFilter}
232
252
  onCategoryFilterChange={setCategoryFilter}
233
253
  categories={categories}
254
+ language={currentLanguage}
255
+ onLanguageChange={setCurrentLanguage}
256
+ availableLanguages={availableLanguages}
234
257
  />
235
258
 
236
259
  {/* View Toggle */}
@@ -10,6 +10,7 @@ import { Calendar, User, UserCheck } from 'lucide-react';
10
10
  import { Image } from '@jhits/plugin-images';
11
11
  import { PostListItem, PostStatus } from '../../types/post';
12
12
  import { PostActionsMenu } from './PostActionsMenu';
13
+ import { LanguageFlags } from './LanguageFlags';
13
14
  import { useSession } from 'next-auth/react';
14
15
 
15
16
  export interface PostTableProps {
@@ -21,7 +22,7 @@ export interface PostTableProps {
21
22
  onDelete: (postId: string) => void;
22
23
  }
23
24
 
24
- function getStatusBadgeColor(status: PostStatus) {
25
+ function getStatusBadgeColor(status: PostStatus | 'not-translated') {
25
26
  switch (status) {
26
27
  case 'published':
27
28
  return 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20';
@@ -31,6 +32,8 @@ function getStatusBadgeColor(status: PostStatus) {
31
32
  return 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20';
32
33
  case 'archived':
33
34
  return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
35
+ case 'not-translated':
36
+ return 'bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/20 italic';
34
37
  default:
35
38
  return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
36
39
  }
@@ -108,6 +111,9 @@ export function PostTable({
108
111
  <th className="px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400">
109
112
  Category
110
113
  </th>
114
+ <th className="px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400">
115
+ Languages
116
+ </th>
111
117
  <th className="px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400">
112
118
  Status
113
119
  </th>
@@ -183,6 +189,11 @@ export function PostTable({
183
189
  </span>
184
190
  </td>
185
191
 
192
+ {/* Languages */}
193
+ <td className="px-6 py-4">
194
+ <LanguageFlags post={post} />
195
+ </td>
196
+
186
197
  {/* Status Badge */}
187
198
  <td className="px-6 py-4">
188
199
  <span
@@ -1,81 +0,0 @@
1
- /**
2
- * Columns Block
3
- * Flex/grid container with configurable column layouts
4
- */
5
-
6
- 'use client';
7
-
8
- import React from 'react';
9
- import { Plus, Trash2 } from 'lucide-react';
10
- import { BlockEditProps, BlockPreviewProps } from '../../../types/block';
11
- import { LayoutContainer } from '../../../views/CanvasEditor/LayoutContainer';
12
- import { COLUMN_LAYOUTS, ColumnLayout } from '../index';
13
- import { Block } from '../../../types/block';
14
-
15
- /**
16
- * Columns Block Edit Component
17
- */
18
- export const ColumnsEdit: React.FC<BlockEditProps & {
19
- childBlocks: Block[];
20
- onChildBlockAdd: (type: string, index: number, containerId: string) => void;
21
- onChildBlockDelete: (blockId: string) => void;
22
- onChildBlockMove: (blockId: string, newIndex: number) => void;
23
- }> = ({
24
- block,
25
- childBlocks,
26
- onChildBlockAdd,
27
- onChildBlockDelete,
28
- onChildBlockMove,
29
- }) => {
30
- // Support both old layout-based system and new dynamic column count
31
- const columnCount = block.data.columnCount as number | undefined;
32
- const layout: ColumnLayout | undefined = block.data.layout as ColumnLayout | undefined;
33
-
34
- // Determine number of columns: use columnCount if set, otherwise derive from layout
35
- let numColumns: number;
36
- let gridClass: string;
37
- let columnWidths: number[];
38
-
39
- // Grid class mapping for Tailwind (must be explicit for dynamic classes)
40
- const gridClassMap: Record<number, string> = {
41
- 1: 'grid-cols-1',
42
- 2: 'grid-cols-2',
43
- 3: 'grid-cols-3',
44
- 4: 'grid-cols-4',
45
- 5: 'grid-cols-5',
46
- 6: 'grid-cols-6',
47
- };
48
-
49
- if (columnCount !== undefined && columnCount > 0) {
50
- // Dynamic column system
51
- numColumns = columnCount;
52
- // Create equal-width columns
53
- const widthPercent = Math.floor(100 / numColumns);
54
- columnWidths = Array(numColumns).fill(widthPercent);
55
- // Use explicit grid class from map, fallback to inline style if needed
56
- gridClass = gridClassMap[numColumns] || \`grid-cols-\${numColumns}\`;
57
- } else if (layout && COLUMN_LAYOUTS[layout]) {
58
- // Legacy layout-based system
59
- const layoutConfig = COLUMN_LAYOUTS[layout];
60
- numColumns = layoutConfig.widths.length;
61
- gridClass = layoutConfig.grid;
62
- columnWidths = layoutConfig.widths;
63
- } else {
64
- // Default to 2 columns
65
- numColumns = 2;
66
- gridClass = 'grid-cols-2';
67
- columnWidths = [50, 50];
68
- }
69
-
70
- // Split child blocks into columns based on columnIndex in meta, or round-robin
71
- const columns: Block[][] = Array.from({ length: numColumns }, () => []);
72
- childBlocks.forEach((childBlock) => {
73
- const columnIndex = childBlock.meta?.columnIndex;
74
- if (typeof columnIndex === 'number' && columnIndex >= 0 && columnIndex < numColumns) {
75
- columns[columnIndex].push(childBlock);
76
- } else {
77
- // Fallback to round-robin if no columnIndex specified
78
- const index = childBlocks.indexOf(childBlock);
79
- columns[index % numColumns].push(childBlock);
80
- }
81
- });