@beforesemicolon/site-builder 0.34.0 → 0.36.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.
@@ -0,0 +1,104 @@
1
+ const { html, effect, state, repeat } = window.BFS.MARKUP
2
+
3
+ const [flashbarMessages, setFlashbarMessages] = state([]) // {id: string, title: string, message: string, type: 'error' | 'warning' | 'info'}
4
+
5
+ const dismissTimers = new Map()
6
+
7
+ effect(() => {
8
+ const messages = flashbarMessages()
9
+
10
+ messages.forEach((message) => {
11
+ if (message.type === 'success' && !dismissTimers.has(message.id)) {
12
+ const timerId = setTimeout(() => {
13
+ setFlashbarMessages((prev) =>
14
+ prev.filter((msg) => msg.id !== message.id)
15
+ )
16
+ dismissTimers.delete(message.id)
17
+ }, 5000)
18
+ dismissTimers.set(message.id, timerId)
19
+ }
20
+ })
21
+ })
22
+
23
+ export const showFlashbar = ({
24
+ id,
25
+ type,
26
+ title,
27
+ message,
28
+ loading,
29
+ dismissable = true,
30
+ }) => {
31
+ id = id || Date.now()
32
+ setFlashbarMessages((prev) => [
33
+ {
34
+ id,
35
+ type,
36
+ title,
37
+ message,
38
+ loading,
39
+ },
40
+ ...prev,
41
+ ])
42
+
43
+ return id
44
+ }
45
+
46
+ export const hideFlashbar = (id) => {
47
+ setFlashbarMessages((prev) => prev.filter((msg) => msg.id !== id))
48
+ }
49
+
50
+ const dismissFlashbar = (message) => {
51
+ // Clear timer if exists
52
+ if (dismissTimers.has(message.id)) {
53
+ clearTimeout(dismissTimers.get(message.id))
54
+ dismissTimers.delete(message.id)
55
+ }
56
+ setFlashbarMessages((prev) => prev.filter((msg) => msg.id !== message.id))
57
+ }
58
+
59
+ // Render flashbar messages
60
+ export const Flashbars = html`
61
+ <div class="flashbar-container">
62
+ ${repeat(
63
+ flashbarMessages,
64
+ (message) => html`
65
+ <div
66
+ class="flashbar flashbar-${message.type}"
67
+ data-flashbar-id="${message.id}"
68
+ >
69
+ <div class="flashbar-content">
70
+ <div class="flashbar-icon">
71
+ ${message.type === 'info' && message.loading
72
+ ? html`<div class="spinner-small"></div>`
73
+ : message.type === 'success'
74
+ ? '✓'
75
+ : message.type === 'error'
76
+ ? '✕'
77
+ : 'ℹ'}
78
+ </div>
79
+ <div class="flashbar-text">
80
+ <strong class="flashbar-title"
81
+ >${message.title}</strong
82
+ >
83
+ ${message.message
84
+ ? html`<p class="flashbar-message">
85
+ ${message.message}
86
+ </p>`
87
+ : ''}
88
+ </div>
89
+ </div>
90
+ ${!message.loading &&
91
+ html`
92
+ <button
93
+ class="flashbar-dismiss"
94
+ onclick="${() => dismissFlashbar(message)}"
95
+ aria-label="Dismiss"
96
+ >
97
+
98
+ </button>
99
+ `}
100
+ </div>
101
+ `
102
+ )}
103
+ </div>
104
+ `
@@ -0,0 +1,44 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>BFS Content Management System</title>
7
+ <link rel="stylesheet" href="/admin/styles.css" />
8
+ <link
9
+ rel="icon"
10
+ type="image/x-icon"
11
+ href="/assets/favicons/favicon.ico"
12
+ />
13
+ <script src="https://unpkg.com/@beforesemicolon/markup/dist/client.js"></script>
14
+ <script src="https://unpkg.com/@beforesemicolon/site-builder/dist/client.js"></script>
15
+ <!-- Auth0 SPA SDK -->
16
+ <script src="https://cdn.auth0.com/js/auth0-spa-js/2.1/auth0-spa-js.production.js"></script>
17
+
18
+ <!-- Enhanced Admin Controls Libraries -->
19
+ <!-- Quill Rich Text Editor -->
20
+ <link
21
+ href="https://cdn.quilljs.com/1.3.6/quill.snow.css"
22
+ rel="stylesheet"
23
+ />
24
+ <script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
25
+
26
+ <!-- Prism Code Syntax Highlighting -->
27
+ <link
28
+ href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css"
29
+ rel="stylesheet"
30
+ />
31
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
32
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
33
+
34
+ <!-- TinyMDE Markdown Editor -->
35
+ <script src="https://unpkg.com/tiny-markdown-editor@0.1.0/dist/tiny-mde.min.js"></script>
36
+ </head>
37
+ <body>
38
+ <!-- /#app -->
39
+ <div id="app"></div>
40
+
41
+ <!-- Scripts -->
42
+ <script type="module" src="/admin/app.js"></script>
43
+ </body>
44
+ </html>
@@ -0,0 +1,123 @@
1
+ const { html, state, when, pick } = window.BFS.MARKUP
2
+
3
+ const [currentModal, setCurrentModal] = state(null) // null | {title: string, size: 'lg' | 'md', description?: string, actionLabel?: string, action?: () => void, loading: boolean, content: string | HTMLTemplate, actions?: Array}
4
+
5
+ export const showModal = (options) => {
6
+ setCurrentModal({
7
+ id: Date.now(),
8
+ dismissable: true,
9
+ ...options,
10
+ })
11
+ }
12
+
13
+ export const hideModal = () => {
14
+ setCurrentModal(null)
15
+ }
16
+
17
+ export const Modal = html`
18
+ ${when(currentModal, () => {
19
+ return html`
20
+ <div class="app-dialog-backdrop">
21
+ <dialog
22
+ ref="dialog"
23
+ class="app-dialog ${pick(currentModal, 'size')}"
24
+ >
25
+ <header class="app-dialog-header">
26
+ <h4>${pick(currentModal, 'title')}</h4>
27
+ ${when(
28
+ pick(currentModal, 'description'),
29
+ () =>
30
+ html`<p class="app-dialog-description">
31
+ ${pick(currentModal, 'description')}
32
+ </p>`
33
+ )}
34
+ </header>
35
+ <div class="app-dialog-content">
36
+ ${pick(currentModal, 'content')}
37
+ </div>
38
+ <footer class="app-dialog-footer">
39
+ ${when(
40
+ pick(currentModal, 'dismissable'),
41
+ () =>
42
+ html` <button
43
+ type="button"
44
+ onclick="${hideModal}"
45
+ >
46
+ Cancel
47
+ </button>`
48
+ )}
49
+ ${when(
50
+ pick(currentModal, 'actions'),
51
+ () => html`
52
+ ${pick(currentModal, 'actions', (actions) =>
53
+ actions.map(
54
+ (action) => html`
55
+ <button
56
+ type="button"
57
+ disabled="${typeof action.loading ===
58
+ 'function'
59
+ ? action.loading()
60
+ : action.loading}"
61
+ onclick="${async () => {
62
+ await action.onClick?.()
63
+ if (
64
+ action.dismiss !== false
65
+ ) {
66
+ hideModal()
67
+ }
68
+ }}"
69
+ >
70
+ <span class="row">
71
+ ${when(
72
+ typeof action.loading ===
73
+ 'function'
74
+ ? action.loading()
75
+ : action.loading,
76
+ () =>
77
+ html`<div
78
+ class="spinner-small"
79
+ ></div> `
80
+ )}
81
+ ${action.label || 'Ok'}
82
+ </span>
83
+ </button>
84
+ `
85
+ )
86
+ )}
87
+ `,
88
+ html`
89
+ <button
90
+ type="button"
91
+ disabled="${currentModal().loading}"
92
+ onclick="${async () => {
93
+ await currentModal().action?.()
94
+ hideModal()
95
+ }}"
96
+ >
97
+ <span class="row">
98
+ ${when(
99
+ currentModal().loading,
100
+ () =>
101
+ html`<div
102
+ class="spinner-small"
103
+ ></div> `
104
+ )}
105
+ ${pick(
106
+ currentModal,
107
+ 'actionLabel',
108
+ (lbl) => lbl || 'Ok'
109
+ )}
110
+ </span>
111
+ </button>
112
+ `
113
+ )}
114
+ </footer>
115
+ </dialog>
116
+ </div>
117
+ `.onMount((tmp) => {
118
+ setTimeout(() => {
119
+ tmp.refs['dialog'][0].showModal()
120
+ })
121
+ })
122
+ })}
123
+ `
@@ -0,0 +1,102 @@
1
+ import {
2
+ config,
3
+ currentPage,
4
+ currentWidget,
5
+ currentWidgetId,
6
+ hasPendingChanges,
7
+ isLocalDevelopment,
8
+ } from './data.js'
9
+
10
+ const { MARKUP, SITE_BUILDER } = window.BFS
11
+ const { html, effect } = MARKUP
12
+ const { parseStyle, inputDefinitionsToObject } = SITE_BUILDER
13
+
14
+ const buildWidgetPreview = (widget, skipStyles = false) => {
15
+ const inputValues = inputDefinitionsToObject(widget?.inputs ?? [])
16
+
17
+ const basePage =
18
+ currentPage() ??
19
+ config()?.pages.find((pg) => pg.type === 'base') ??
20
+ null
21
+ const env = {
22
+ assetsOrigin: window.location.origin + '/',
23
+ prod: !isLocalDevelopment,
24
+ ...basePage,
25
+ }
26
+
27
+ const content = widget.render({ ...inputValues, env }) ?? ''
28
+
29
+ let styles = ''
30
+ if (!skipStyles && widget.style) {
31
+ const styleObject =
32
+ typeof widget.style === 'function'
33
+ ? widget.style({ ...inputValues, env })
34
+ : widget.style
35
+ styles = parseStyle(styleObject, widget.cssSelector ?? '')
36
+ }
37
+ return { content, styles }
38
+ }
39
+
40
+ let commonHeader = ''
41
+
42
+ const getHeadContent = async () => {
43
+ if (commonHeader) {
44
+ return commonHeader
45
+ }
46
+
47
+ const res = await fetch(location.origin)
48
+ const str = await res.text()
49
+
50
+ const m = str.match(new RegExp('<head>(.*)</head>'))
51
+
52
+ if (m) {
53
+ commonHeader = m[1]
54
+ }
55
+
56
+ return commonHeader
57
+ }
58
+
59
+ export const WidgetPreview = html`
60
+ <iframe
61
+ ref="iframe"
62
+ style="width: 100%; height: 100%; border: none; background: none;"
63
+ ></iframe>
64
+ `.onMount((temp) => {
65
+ const [iframe] = temp.refs['iframe']
66
+
67
+ return effect(() => {
68
+ hasPendingChanges() // just to trigger updates
69
+ const w = currentWidget()
70
+ const wId = currentWidgetId()
71
+
72
+ if (w) {
73
+ const { content, styles } = buildWidgetPreview(
74
+ w,
75
+ Boolean(iframe.srcdoc)
76
+ )
77
+
78
+ getHeadContent().then((headContent) => {
79
+ if (iframe.srcdoc) {
80
+ const doc =
81
+ iframe.contentDocument || iframe.contentWindow?.document
82
+ doc.body.innerHTML = `<widget id="${wId}" style="display: block;">${content}</widget>`
83
+ } else {
84
+ iframe.srcdoc = `
85
+ <!DOCTYPE html>
86
+ <html lang="en">
87
+ <head>
88
+ ${headContent}
89
+ <style>${styles}</style>
90
+ </head>
91
+ <body>
92
+ <widget id="${wId}" style="display: block;">${content}</widget>
93
+ </body>
94
+ </html>
95
+ `
96
+ }
97
+ })
98
+ } else {
99
+ iframe.srcdoc = ''
100
+ }
101
+ })
102
+ })
@@ -0,0 +1,197 @@
1
+ import {
2
+ config,
3
+ currentPage,
4
+ currentWidget,
5
+ currentWidgetId,
6
+ hasPendingChanges,
7
+ isLocalDevelopment,
8
+ loadWidget,
9
+ widgetElements,
10
+ widgets,
11
+ } from './data.js'
12
+
13
+ const { html, element, effect } = window.BFS.MARKUP
14
+
15
+ const currentPageUrl = () => currentPage()?.url ?? ''
16
+
17
+ export const getWidgetById = (id) => widgets.get(id)
18
+
19
+ const getHighestZIndex = (root) => {
20
+ let max = 0
21
+ const nodes = [root, ...root.querySelectorAll('*')]
22
+ nodes.forEach((node) => {
23
+ const value = window.getComputedStyle(node).zIndex
24
+ const numeric = Number.parseInt(value, 10)
25
+ if (Number.isFinite(numeric)) {
26
+ max = Math.max(max, numeric)
27
+ }
28
+ })
29
+ return max
30
+ }
31
+
32
+ const initPreviewContent = (e) => {
33
+ const currentIframe = e.target
34
+ if (currentIframe) {
35
+ const iframeDoc =
36
+ currentIframe.contentDocument ||
37
+ currentIframe.contentWindow?.document
38
+
39
+ if (!iframeDoc) {
40
+ return
41
+ }
42
+
43
+ const style = element('style', {
44
+ attributes: {
45
+ class: 'cms-widget-preview',
46
+ },
47
+ textContent: `
48
+ widget {
49
+ position: relative;
50
+ transition: all 0.2s ease;
51
+ cursor: pointer;
52
+ box-sizing: border-box;
53
+ }
54
+
55
+ widget .__overlay__ {
56
+ cursor: pointer;
57
+ display: inline-block;
58
+ position: absolute;
59
+ top: 0;
60
+ left: 0;
61
+ width: 100%;
62
+ height: 100%;
63
+ background: #007bff7a;
64
+ display: none;
65
+ border: 2px solid #007bff !important;
66
+ }
67
+
68
+ widget:hover .__overlay__ {
69
+ display: block;
70
+ }
71
+
72
+ widget.cms-widget-selected .__overlay__ {
73
+ border: 2px solid #28a745 !important;
74
+ background: none;
75
+ display: block;
76
+ }
77
+ `,
78
+ })
79
+ iframeDoc.head.appendChild(style)
80
+
81
+ let selectedWidget
82
+
83
+ iframeDoc.querySelectorAll('widget').forEach((el) => {
84
+ const widgetId = el.getAttribute('id')
85
+
86
+ if (widgetId) {
87
+ widgetElements.set(widgetId, el)
88
+
89
+ requestAnimationFrame(() => {
90
+ const highestZIndex = getHighestZIndex(el)
91
+ const overlayZIndex = highestZIndex + 1
92
+
93
+ el.appendChild(
94
+ element('span', {
95
+ attributes: {
96
+ class: '__overlay__',
97
+ style: `z-index: ${overlayZIndex};`,
98
+ onclick: (e) => {
99
+ e.preventDefault()
100
+ e.stopPropagation()
101
+
102
+ selectedWidget?.classList.remove(
103
+ 'cms-widget-selected'
104
+ )
105
+
106
+ el.classList.add('cms-widget-selected')
107
+
108
+ // Scroll widget into view if needed
109
+ el.scrollIntoView({
110
+ behavior: 'smooth',
111
+ block: 'center',
112
+ })
113
+
114
+ selectedWidget = el
115
+
116
+ loadWidget(widgetId)
117
+ },
118
+ },
119
+ })
120
+ )
121
+ })
122
+ }
123
+ })
124
+ }
125
+ }
126
+
127
+ export const Preview = html`
128
+ <iframe
129
+ src="${currentPageUrl}"
130
+ onload="${initPreviewContent}"
131
+ style="width: 100%; height: 100%; border: none; background: none;"
132
+ id="preview-iframe"
133
+ ></iframe>
134
+ `.onMount(() => {
135
+ return effect(() => {
136
+ hasPendingChanges() // just to force trigger update
137
+ const widget = currentWidget()
138
+ const widgetId = currentWidgetId()
139
+
140
+ if (!widgetId) return
141
+
142
+ try {
143
+ const widgetElement = widgetElements.get(widgetId)
144
+
145
+ if (!widgetElement) {
146
+ console.error(`Widget element not found for ${widgetId}`)
147
+ return
148
+ }
149
+
150
+ function extractValue(input) {
151
+ if (input.type === 'list') {
152
+ return (input.definitions ?? []).map(extractValue)
153
+ }
154
+
155
+ if (input.type === 'group') {
156
+ return (input.definitions ?? []).reduce((acc, i) => {
157
+ return {
158
+ ...acc,
159
+ [i.name]: extractValue(i),
160
+ }
161
+ }, {})
162
+ }
163
+
164
+ return input.value
165
+ }
166
+
167
+ const inputValues = (widget?.inputs ?? []).reduce((acc, input) => {
168
+ return {
169
+ ...acc,
170
+ [input.name]: extractValue(input),
171
+ }
172
+ }, {})
173
+
174
+ const basePage =
175
+ currentPage() ??
176
+ config()?.pages.find((p) => p.type === 'base') ??
177
+ null
178
+
179
+ const env = {
180
+ assetsOrigin: window.location.origin + '/',
181
+ prod: !isLocalDevelopment,
182
+ ...basePage,
183
+ }
184
+
185
+ requestAnimationFrame(() => {
186
+ const newHTML = widget.render({ ...inputValues, env })
187
+
188
+ const overlay = widgetElement.querySelector('.__overlay__')
189
+
190
+ widgetElement.innerHTML = newHTML
191
+ widgetElement.appendChild(overlay)
192
+ })
193
+ } catch (error) {
194
+ console.error('Error updating preview:', error)
195
+ }
196
+ })
197
+ })