@uniweb/kit 0.1.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.
Files changed (37) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +345 -0
  3. package/package.json +47 -0
  4. package/src/components/Asset/Asset.jsx +161 -0
  5. package/src/components/Asset/index.js +1 -0
  6. package/src/components/Disclaimer/Disclaimer.jsx +198 -0
  7. package/src/components/Disclaimer/index.js +1 -0
  8. package/src/components/FileLogo/FileLogo.jsx +148 -0
  9. package/src/components/FileLogo/index.js +1 -0
  10. package/src/components/Icon/Icon.jsx +214 -0
  11. package/src/components/Icon/index.js +1 -0
  12. package/src/components/Image/Image.jsx +194 -0
  13. package/src/components/Image/index.js +1 -0
  14. package/src/components/Link/Link.jsx +261 -0
  15. package/src/components/Link/index.js +1 -0
  16. package/src/components/Media/Media.jsx +322 -0
  17. package/src/components/Media/index.js +1 -0
  18. package/src/components/MediaIcon/MediaIcon.jsx +95 -0
  19. package/src/components/MediaIcon/index.js +1 -0
  20. package/src/components/SafeHtml/SafeHtml.jsx +93 -0
  21. package/src/components/SafeHtml/index.js +1 -0
  22. package/src/components/Section/Render.jsx +245 -0
  23. package/src/components/Section/Section.jsx +131 -0
  24. package/src/components/Section/index.js +3 -0
  25. package/src/components/Section/renderers/Alert.jsx +101 -0
  26. package/src/components/Section/renderers/Code.jsx +70 -0
  27. package/src/components/Section/renderers/Details.jsx +77 -0
  28. package/src/components/Section/renderers/Divider.jsx +42 -0
  29. package/src/components/Section/renderers/Table.jsx +55 -0
  30. package/src/components/Section/renderers/index.js +11 -0
  31. package/src/components/Text/Text.jsx +207 -0
  32. package/src/components/Text/index.js +14 -0
  33. package/src/hooks/index.js +1 -0
  34. package/src/hooks/useWebsite.js +77 -0
  35. package/src/index.js +69 -0
  36. package/src/styles/index.css +8 -0
  37. package/src/utils/index.js +104 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Table Renderer
3
+ *
4
+ * Renders HTML tables from structured content.
5
+ *
6
+ * @module @uniweb/kit/Section/renderers/Table
7
+ */
8
+
9
+ import React from 'react'
10
+ import { cn } from '../../../utils/index.js'
11
+ import { SafeHtml } from '../../SafeHtml/index.js'
12
+
13
+ /**
14
+ * Table - Table renderer
15
+ *
16
+ * @param {Object} props
17
+ * @param {Array} props.content - Table content as rows/cells array
18
+ * @param {string} [props.className] - Additional CSS classes
19
+ */
20
+ export function Table({ content, className, ...props }) {
21
+ if (!content || !Array.isArray(content)) return null
22
+
23
+ return (
24
+ <div className={cn('overflow-x-auto', className)} {...props}>
25
+ <table className="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
26
+ <tbody className="divide-y divide-gray-200 bg-white">
27
+ {content.map((row, rowIndex) => (
28
+ <tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
29
+ {row.content?.map((cell, cellIndex) => {
30
+ const CellTag = cell.type === 'tableHeader' ? 'th' : 'td'
31
+ const cellContent = cell.content?.[0]?.content?.[0]?.text || ''
32
+
33
+ return (
34
+ <CellTag
35
+ key={cellIndex}
36
+ className={cn(
37
+ 'px-4 py-2 text-sm',
38
+ cell.type === 'tableHeader'
39
+ ? 'font-medium text-gray-900 bg-gray-100'
40
+ : 'text-gray-600'
41
+ )}
42
+ >
43
+ <SafeHtml value={cellContent} as="span" />
44
+ </CellTag>
45
+ )
46
+ })}
47
+ </tr>
48
+ ))}
49
+ </tbody>
50
+ </table>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ export default Table
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Section Renderers
3
+ *
4
+ * Individual content type renderers for the Section component.
5
+ */
6
+
7
+ export { Code } from './Code.jsx'
8
+ export { Alert, Warning } from './Alert.jsx'
9
+ export { Table } from './Table.jsx'
10
+ export { Details } from './Details.jsx'
11
+ export { Divider } from './Divider.jsx'
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Text Component
3
+ *
4
+ * Smart typography component for rendering content from semantic-parser.
5
+ * Handles single strings or arrays of paragraphs with automatic semantic
6
+ * tag selection and empty content filtering.
7
+ *
8
+ * Security Model:
9
+ * - Assumes content is ALREADY SANITIZED at the engine level
10
+ * - Does NOT sanitize HTML (that's the engine's responsibility)
11
+ * - Trusts the data it receives and renders it as-is
12
+ *
13
+ * @module @uniweb/kit/Text
14
+ */
15
+
16
+ import React, { memo } from 'react'
17
+ import { cn } from '../../utils/index.js'
18
+
19
+ /**
20
+ * Text - Smart typography component
21
+ *
22
+ * @param {Object} props
23
+ * @param {string|string[]} props.text - Content to render (string or array of strings)
24
+ * @param {string} [props.as='p'] - HTML tag: 'h1'-'h6', 'p', 'div', 'span'
25
+ * @param {boolean} [props.html=true] - Render as HTML (true) or plain text (false)
26
+ * @param {string} [props.className] - CSS classes for wrapper/elements
27
+ * @param {string} [props.lineAs] - Tag for each line in array. Defaults: 'div' for headings, 'p' for others
28
+ *
29
+ * @example
30
+ * // Simple paragraph
31
+ * <Text text="Hello World" />
32
+ *
33
+ * @example
34
+ * // Heading
35
+ * <Text text="Welcome" as="h1" />
36
+ *
37
+ * @example
38
+ * // Multi-line heading (wrapped in single h1)
39
+ * <Text text={["Welcome to", "Our Platform"]} as="h1" />
40
+ *
41
+ * @example
42
+ * // Multiple paragraphs (each gets its own <p>)
43
+ * <Text text={["First paragraph", "Second paragraph"]} />
44
+ *
45
+ * @example
46
+ * // Rich HTML content (assumes pre-sanitized)
47
+ * <Text text="Hello <strong>World</strong>" />
48
+ *
49
+ * @example
50
+ * // Plain text (HTML tags shown as text)
51
+ * <Text text="Show <strong>tags</strong>" html={false} />
52
+ */
53
+ export const Text = memo(function Text({
54
+ text,
55
+ as = 'p',
56
+ html = true,
57
+ className,
58
+ lineAs,
59
+ ...props
60
+ }) {
61
+ const isArray = Array.isArray(text)
62
+ const Tag = as
63
+ const isHeading = /^h[1-6]$/.test(as)
64
+
65
+ // Single string input
66
+ if (!isArray) {
67
+ if (!text || (typeof text === 'string' && text.trim() === '')) {
68
+ return null
69
+ }
70
+
71
+ if (html) {
72
+ return (
73
+ <Tag
74
+ className={className}
75
+ dangerouslySetInnerHTML={{ __html: text }}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ return (
82
+ <Tag className={className} {...props}>
83
+ {text}
84
+ </Tag>
85
+ )
86
+ }
87
+
88
+ // Array input - filter empty content
89
+ const filteredText = text.filter(
90
+ (item) => typeof item === 'string' && item.trim() !== ''
91
+ )
92
+
93
+ if (filteredText.length === 0) {
94
+ return null
95
+ }
96
+
97
+ // Determine line wrapper tag with smart defaults
98
+ const LineTag = lineAs || (isHeading ? 'div' : 'p')
99
+
100
+ // Multi-line heading: wrap all lines in single heading tag
101
+ if (isHeading) {
102
+ return (
103
+ <Tag className={className} {...props}>
104
+ {filteredText.map((line, i) => {
105
+ if (html) {
106
+ return (
107
+ <LineTag
108
+ key={i}
109
+ dangerouslySetInnerHTML={{ __html: line }}
110
+ />
111
+ )
112
+ }
113
+ return <LineTag key={i}>{line}</LineTag>
114
+ })}
115
+ </Tag>
116
+ )
117
+ }
118
+
119
+ // Non-heading arrays: render each line as separate element
120
+ return (
121
+ <>
122
+ {filteredText.map((line, i) => {
123
+ if (html) {
124
+ return (
125
+ <LineTag
126
+ key={i}
127
+ className={className}
128
+ dangerouslySetInnerHTML={{ __html: line }}
129
+ {...props}
130
+ />
131
+ )
132
+ }
133
+ return (
134
+ <LineTag key={i} className={className} {...props}>
135
+ {line}
136
+ </LineTag>
137
+ )
138
+ })}
139
+ </>
140
+ )
141
+ })
142
+
143
+ // ============================================================================
144
+ // Semantic Wrapper Components
145
+ // ============================================================================
146
+
147
+ /**
148
+ * H1 - Heading level 1
149
+ * @example
150
+ * <H1 text="Main Title" />
151
+ * <H1 text={["Multi-line", "Title"]} />
152
+ */
153
+ export const H1 = (props) => <Text {...props} as="h1" />
154
+
155
+ /**
156
+ * H2 - Heading level 2
157
+ */
158
+ export const H2 = (props) => <Text {...props} as="h2" />
159
+
160
+ /**
161
+ * H3 - Heading level 3
162
+ */
163
+ export const H3 = (props) => <Text {...props} as="h3" />
164
+
165
+ /**
166
+ * H4 - Heading level 4
167
+ */
168
+ export const H4 = (props) => <Text {...props} as="h4" />
169
+
170
+ /**
171
+ * H5 - Heading level 5
172
+ */
173
+ export const H5 = (props) => <Text {...props} as="h5" />
174
+
175
+ /**
176
+ * H6 - Heading level 6
177
+ */
178
+ export const H6 = (props) => <Text {...props} as="h6" />
179
+
180
+ /**
181
+ * P - Paragraph
182
+ * @example
183
+ * <P text="A paragraph of content" />
184
+ * <P text={["First paragraph", "Second paragraph"]} />
185
+ */
186
+ export const P = (props) => <Text {...props} as="p" />
187
+
188
+ /**
189
+ * Span - Inline text
190
+ */
191
+ export const Span = (props) => <Text {...props} as="span" />
192
+
193
+ /**
194
+ * Div - Block container
195
+ * @example
196
+ * <Div text={["Item 1", "Item 2"]} lineAs="span" />
197
+ */
198
+ export const Div = (props) => <Text {...props} as="div" />
199
+
200
+ /**
201
+ * PlainText - Text with HTML rendering disabled
202
+ * @example
203
+ * <PlainText text="Show <strong>tags</strong> as text" />
204
+ */
205
+ export const PlainText = (props) => <Text {...props} html={false} />
206
+
207
+ export default Text
@@ -0,0 +1,14 @@
1
+ export {
2
+ Text,
3
+ default,
4
+ H1,
5
+ H2,
6
+ H3,
7
+ H4,
8
+ H5,
9
+ H6,
10
+ P,
11
+ Span,
12
+ Div,
13
+ PlainText
14
+ } from './Text.jsx'
@@ -0,0 +1 @@
1
+ export { useWebsite, default } from './useWebsite.js'
@@ -0,0 +1,77 @@
1
+ /**
2
+ * useWebsite Hook
3
+ *
4
+ * Provides access to the current website instance and common utilities.
5
+ * This is the primary hook for kit components to interact with the runtime.
6
+ *
7
+ * @example
8
+ * function MyComponent() {
9
+ * const { website, localize, makeHref } = useWebsite()
10
+ * return <a href={makeHref('/about')}>{localize({ en: 'About', fr: 'À propos' })}</a>
11
+ * }
12
+ */
13
+
14
+ import { getUniweb } from '@uniweb/core'
15
+
16
+ /**
17
+ * Get the current website instance and utilities
18
+ * @returns {Object} Website utilities
19
+ */
20
+ export function useWebsite() {
21
+ const uniweb = getUniweb()
22
+
23
+ if (!uniweb) {
24
+ console.warn('[Kit] Runtime not initialized. Components require @uniweb/runtime.')
25
+ return {
26
+ website: null,
27
+ localize: (val, defaultVal = '') => defaultVal,
28
+ makeHref: (href) => href,
29
+ getLanguage: () => 'en',
30
+ getLanguages: () => []
31
+ }
32
+ }
33
+
34
+ const website = uniweb.activeWebsite
35
+
36
+ return {
37
+ /**
38
+ * The active Website instance
39
+ */
40
+ website,
41
+
42
+ /**
43
+ * Localize a multilingual value
44
+ * @param {Object|string} value - Object with language keys or string
45
+ * @param {string} defaultVal - Fallback value
46
+ * @returns {string}
47
+ */
48
+ localize: (value, defaultVal = '') => website.localize(value, defaultVal),
49
+
50
+ /**
51
+ * Transform a href (handles topic: protocol, etc.)
52
+ * @param {string} href
53
+ * @returns {string}
54
+ */
55
+ makeHref: (href) => website.makeHref(href),
56
+
57
+ /**
58
+ * Get current language code
59
+ * @returns {string}
60
+ */
61
+ getLanguage: () => website.getLanguage(),
62
+
63
+ /**
64
+ * Get available languages
65
+ * @returns {Array<{label: string, value: string}>}
66
+ */
67
+ getLanguages: () => website.getLanguages(),
68
+
69
+ /**
70
+ * Get routing components (Link, useNavigate, etc.)
71
+ * @returns {Object}
72
+ */
73
+ getRoutingComponents: () => website.getRoutingComponents()
74
+ }
75
+ }
76
+
77
+ export default useWebsite
package/src/index.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @uniweb/kit
3
+ *
4
+ * Standard component library for Uniweb foundations.
5
+ *
6
+ * @example
7
+ * import { Link, Image, useWebsite } from '@uniweb/kit'
8
+ *
9
+ * function MyComponent() {
10
+ * const { localize } = useWebsite()
11
+ * return (
12
+ * <Link to="/about">
13
+ * <Image src="/logo.png" alt="Logo" />
14
+ * {localize({ en: 'About', fr: 'À propos' })}
15
+ * </Link>
16
+ * )
17
+ * }
18
+ */
19
+
20
+ // ============================================================================
21
+ // Components
22
+ // ============================================================================
23
+
24
+ // Primitives
25
+ export { Link } from './components/Link/index.js'
26
+ export { Image } from './components/Image/index.js'
27
+ export { SafeHtml } from './components/SafeHtml/index.js'
28
+ export { Icon } from './components/Icon/index.js'
29
+
30
+ // Typography
31
+ export {
32
+ Text,
33
+ H1, H2, H3, H4, H5, H6,
34
+ P, Span, Div,
35
+ PlainText
36
+ } from './components/Text/index.js'
37
+
38
+ // Media
39
+ export { Media } from './components/Media/index.js'
40
+ export { FileLogo } from './components/FileLogo/index.js'
41
+ export { MediaIcon } from './components/MediaIcon/index.js'
42
+
43
+ // Content
44
+ export { Section, Render } from './components/Section/index.js'
45
+ export { Code, Alert, Warning, Table, Details, Divider } from './components/Section/renderers/index.js'
46
+
47
+ // Utilities
48
+ export { Asset } from './components/Asset/index.js'
49
+ export { Disclaimer } from './components/Disclaimer/index.js'
50
+
51
+ // ============================================================================
52
+ // Hooks
53
+ // ============================================================================
54
+
55
+ export { useWebsite } from './hooks/index.js'
56
+
57
+ // ============================================================================
58
+ // Utilities
59
+ // ============================================================================
60
+
61
+ export {
62
+ cn,
63
+ twMerge,
64
+ twJoin,
65
+ stripTags,
66
+ isExternalUrl,
67
+ isFileUrl,
68
+ detectMediaType
69
+ } from './utils/index.js'
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @uniweb/kit styles
3
+ *
4
+ * Import this file to include kit component styles:
5
+ * import '@uniweb/kit/styles'
6
+ */
7
+
8
+ /* Base component styles will be added here as components are migrated */
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Kit Utilities
3
+ *
4
+ * Common utility functions for kit components.
5
+ */
6
+
7
+ import { twMerge, twJoin } from 'tailwind-merge'
8
+
9
+ // Re-export tailwind-merge utilities
10
+ export { twMerge, twJoin }
11
+
12
+ /**
13
+ * Merge class names with Tailwind CSS conflict resolution
14
+ * @param {...string} classes - Class names to merge
15
+ * @returns {string}
16
+ */
17
+ export function cn(...classes) {
18
+ return twMerge(twJoin(classes.filter(Boolean)))
19
+ }
20
+
21
+ /**
22
+ * Strip HTML tags from a string
23
+ * @param {string} html - HTML string
24
+ * @returns {string} Plain text
25
+ */
26
+ export function stripTags(html) {
27
+ if (!html || typeof html !== 'string') return ''
28
+
29
+ // Use DOMParser for safe HTML entity decoding
30
+ if (typeof DOMParser !== 'undefined') {
31
+ const doc = new DOMParser().parseFromString(html, 'text/html')
32
+ return doc.body.textContent || ''
33
+ }
34
+
35
+ // Fallback: simple regex (less accurate but works in Node)
36
+ return html.replace(/<[^>]*>/g, '')
37
+ }
38
+
39
+ /**
40
+ * Check if a URL is external (different origin)
41
+ * @param {string} url
42
+ * @returns {boolean}
43
+ */
44
+ export function isExternalUrl(url) {
45
+ if (!url || typeof url !== 'string') return false
46
+ if (url.startsWith('/') || url.startsWith('#')) return false
47
+
48
+ try {
49
+ const urlObj = new URL(url, window.location.origin)
50
+ return urlObj.origin !== window.location.origin
51
+ } catch {
52
+ return false
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Check if a URL points to a downloadable file
58
+ * @param {string} url
59
+ * @returns {boolean}
60
+ */
61
+ export function isFileUrl(url) {
62
+ if (!url || typeof url !== 'string') return false
63
+
64
+ const fileExtensions = [
65
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
66
+ '.zip', '.rar', '.7z', '.tar', '.gz',
67
+ '.mp3', '.wav', '.ogg', '.flac',
68
+ '.mp4', '.avi', '.mov', '.wmv', '.webm',
69
+ '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp',
70
+ '.txt', '.csv', '.json', '.xml'
71
+ ]
72
+
73
+ const lowerUrl = url.toLowerCase()
74
+ return fileExtensions.some(ext => lowerUrl.includes(ext))
75
+ }
76
+
77
+ /**
78
+ * Detect media type from URL
79
+ * @param {string} url
80
+ * @returns {'youtube'|'vimeo'|'video'|'image'|'audio'|'unknown'}
81
+ */
82
+ export function detectMediaType(url) {
83
+ if (!url) return 'unknown'
84
+
85
+ const lowerUrl = url.toLowerCase()
86
+
87
+ if (lowerUrl.includes('youtube.com') || lowerUrl.includes('youtu.be')) {
88
+ return 'youtube'
89
+ }
90
+ if (lowerUrl.includes('vimeo.com')) {
91
+ return 'vimeo'
92
+ }
93
+ if (/\.(mp4|webm|ogg|mov|avi)/.test(lowerUrl)) {
94
+ return 'video'
95
+ }
96
+ if (/\.(mp3|wav|ogg|flac|aac)/.test(lowerUrl)) {
97
+ return 'audio'
98
+ }
99
+ if (/\.(jpg|jpeg|png|gif|svg|webp|avif)/.test(lowerUrl)) {
100
+ return 'image'
101
+ }
102
+
103
+ return 'unknown'
104
+ }