@rimelight/cms 0.0.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 (87) hide show
  1. package/README.md +19 -0
  2. package/package.json +69 -0
  3. package/src/astro/BlockRenderer.astro +111 -0
  4. package/src/astro/PageRenderer.astro +23 -0
  5. package/src/astro/blocks/CalloutBlock.astro +78 -0
  6. package/src/astro/blocks/CardBlock.astro +43 -0
  7. package/src/astro/blocks/CardsBlock.astro +24 -0
  8. package/src/astro/blocks/CodeBlock.astro +16 -0
  9. package/src/astro/blocks/DialogueBlock.astro +23 -0
  10. package/src/astro/blocks/FileTreeBlock.astro +45 -0
  11. package/src/astro/blocks/ImageBlock.astro +17 -0
  12. package/src/astro/blocks/OrderedListBlock.astro +55 -0
  13. package/src/astro/blocks/ParagraphBlock.astro +65 -0
  14. package/src/astro/blocks/SceneBlock.astro +65 -0
  15. package/src/astro/blocks/ScriptBlock.astro +59 -0
  16. package/src/astro/blocks/SectionBlock.astro +46 -0
  17. package/src/astro/blocks/StepItemBlock.astro +25 -0
  18. package/src/astro/blocks/StepsBlock.astro +14 -0
  19. package/src/astro/blocks/TabItemBlock.astro +19 -0
  20. package/src/astro/blocks/TableBlock.astro +49 -0
  21. package/src/astro/blocks/TabsBlock.astro +67 -0
  22. package/src/astro/blocks/UnorderedListBlock.astro +55 -0
  23. package/src/auth/editor-guards.ts +148 -0
  24. package/src/auth/filter-blocks.ts +44 -0
  25. package/src/auth/filter-templates.ts +81 -0
  26. package/src/auth/permissions.ts +40 -0
  27. package/src/cache/purge.ts +37 -0
  28. package/src/client/components/RimelightBlock.tsx +487 -0
  29. package/src/client/components/RimelightBlockPicker.tsx +305 -0
  30. package/src/client/components/RimelightEditor.tsx +131 -0
  31. package/src/client/components/RimelightEditorLayout.tsx +143 -0
  32. package/src/client/components/RimelightPreview.tsx +354 -0
  33. package/src/client/components/RimelightPropertiesPanel.tsx +408 -0
  34. package/src/client/components/RimelightTemplateEditor.tsx +245 -0
  35. package/src/client/components/editors/CalloutEditor.tsx +185 -0
  36. package/src/client/components/editors/CardEditor.tsx +60 -0
  37. package/src/client/components/editors/CardsEditor.tsx +132 -0
  38. package/src/client/components/editors/CodeEditor.tsx +101 -0
  39. package/src/client/components/editors/DialogueEditor.tsx +52 -0
  40. package/src/client/components/editors/FileTreeEditor.tsx +65 -0
  41. package/src/client/components/editors/ImageEditor.tsx +70 -0
  42. package/src/client/components/editors/ParagraphEditor.tsx +314 -0
  43. package/src/client/components/editors/SceneEditor.tsx +245 -0
  44. package/src/client/components/editors/ScriptEditor.tsx +245 -0
  45. package/src/client/components/editors/SectionEditor.tsx +139 -0
  46. package/src/client/components/editors/StepItemEditor.tsx +117 -0
  47. package/src/client/components/editors/StepsEditor.tsx +105 -0
  48. package/src/client/components/editors/TabItemEditor.tsx +113 -0
  49. package/src/client/components/editors/TableEditor.tsx +181 -0
  50. package/src/client/components/editors/TabsEditor.tsx +105 -0
  51. package/src/client/components/index.ts +7 -0
  52. package/src/client/loader.ts +19 -0
  53. package/src/client/state/autosave.ts +122 -0
  54. package/src/client/state/editor-store.ts +411 -0
  55. package/src/client/state/history.ts +251 -0
  56. package/src/client/state/lock-manager.ts +89 -0
  57. package/src/client/styles/cms-editor.css +653 -0
  58. package/src/client/toast.ts +12 -0
  59. package/src/client/utils/sortable.ts +184 -0
  60. package/src/client/utils/splitpane.ts +87 -0
  61. package/src/client/utils/tree-sanitizer.ts +33 -0
  62. package/src/core/page-definitions.ts +502 -0
  63. package/src/core/registry.ts +99 -0
  64. package/src/core/template-sync.ts +75 -0
  65. package/src/core/types/blocks.ts +367 -0
  66. package/src/core/types/inline.ts +23 -0
  67. package/src/core/types/pages.ts +36 -0
  68. package/src/core/types/templates.ts +32 -0
  69. package/src/core/types/versioning.ts +44 -0
  70. package/src/core/validator.ts +28 -0
  71. package/src/env.d.ts +4 -0
  72. package/src/index.ts +47 -0
  73. package/src/markdown/index.ts +2 -0
  74. package/src/markdown/parser.ts +347 -0
  75. package/src/markdown/serializer.ts +219 -0
  76. package/src/migration.ts +155 -0
  77. package/src/schema/index.ts +8 -0
  78. package/src/schema/page_draft_locks.ts +13 -0
  79. package/src/schema/page_drafts.ts +18 -0
  80. package/src/schema/page_templates.ts +27 -0
  81. package/src/schema/page_version_approvals.ts +12 -0
  82. package/src/schema/page_version_comments.ts +14 -0
  83. package/src/schema/page_versions.ts +34 -0
  84. package/src/schema/pages.ts +42 -0
  85. package/src/schema/search.ts +23 -0
  86. package/src/search/query.ts +10 -0
  87. package/src/search/sync.ts +32 -0
@@ -0,0 +1,12 @@
1
+ export {
2
+ toasts,
3
+ toast,
4
+ addToast,
5
+ removeToast,
6
+ clearToasts,
7
+ showErrorToast,
8
+ showSuccessToast,
9
+ type ToastItem,
10
+ type Toast,
11
+ type ToastInput
12
+ } from "@rimelight/ui/components/toast/store.ts"
@@ -0,0 +1,184 @@
1
+ import Sortable from "sortablejs"
2
+
3
+ const sortableProto: Record<string, any> | undefined = Sortable?.prototype
4
+
5
+ if (typeof window !== "undefined" && sortableProto && !sortableProto.patchedParentNodeGuard) {
6
+ sortableProto.patchedParentNodeGuard = true
7
+ const originalOnDragOver = sortableProto["_onDragOver"]
8
+ if (typeof originalOnDragOver === "function") {
9
+ sortableProto["_onDragOver"] = function (evt: Event) {
10
+ if (evt && evt.target instanceof Element) {
11
+ const el = evt.target
12
+ if (typeof document !== "undefined" && !document.contains(el)) {
13
+ return
14
+ }
15
+ }
16
+ try {
17
+ originalOnDragOver.call(this, evt)
18
+ } catch (err: unknown) {
19
+ if (
20
+ err instanceof TypeError ||
21
+ (err instanceof Error && err.message.includes("parentNode"))
22
+ ) {
23
+ return
24
+ }
25
+ throw err
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ export interface SortableOptions {
32
+ onReorder: (oldIndex: number, newIndex: number) => void
33
+ onAdd?: (
34
+ blockId: string,
35
+ newIndex: number,
36
+ fromContainerId?: string,
37
+ itemEl?: HTMLElement
38
+ ) => void
39
+ onRemove?: (blockId: string, oldIndex: number) => void
40
+ handle?: string
41
+ filter?: string
42
+ group?: string
43
+ }
44
+
45
+ export function initBlockSortable(container: HTMLElement, options: SortableOptions): Sortable {
46
+ const runCleanup = (specificItem?: HTMLElement | Element) => {
47
+ const doClean = () => cleanAllSortableClasses()
48
+ cleanAllSortableClasses(specificItem)
49
+ doClean()
50
+ requestAnimationFrame(doClean)
51
+ setTimeout(doClean, 50)
52
+ setTimeout(doClean, 150)
53
+ setTimeout(doClean, 300)
54
+ setTimeout(doClean, 600)
55
+ }
56
+
57
+ return Sortable.create(container, {
58
+ group: {
59
+ name: options.group || "cms-blocks",
60
+ pull: true,
61
+ put: (to, _from, dragEl) => {
62
+ if (!to || !to.el || !dragEl) return false
63
+ // Prevent a container block from being dropped into itself or its own descendants
64
+ if (dragEl === to.el || dragEl.contains(to.el)) return false
65
+ return true
66
+ }
67
+ },
68
+ animation: 150,
69
+ handle: options.handle || ".cms-drag-handle, .cms-grip-btn",
70
+ filter: options.filter || ".locked", // Templated or read-only blocks filtered out from drag
71
+ forceFallback: false,
72
+ fallbackOnBody: false,
73
+ fallbackTolerance: 3,
74
+ ghostClass: "cms-sortable-ghost",
75
+ chosenClass: "cms-sortable-chosen",
76
+ dragClass: "cms-sortable-drag",
77
+ swapThreshold: 0.65,
78
+ emptyInsertThreshold: 50,
79
+ onStart: () => {},
80
+ onUnchoose: () => {
81
+ runCleanup()
82
+ },
83
+ onSpill: () => {
84
+ runCleanup()
85
+ },
86
+ onEnd: (evt) => {
87
+ runCleanup(evt.item)
88
+
89
+ if (
90
+ evt.from === evt.to &&
91
+ evt.oldIndex !== undefined &&
92
+ evt.newIndex !== undefined &&
93
+ evt.oldIndex !== evt.newIndex
94
+ ) {
95
+ const oldIdx = evt.oldIndex
96
+ const newIdx = evt.newIndex
97
+ // Remove SortableJS-placed node; SolidJS will re-render from state
98
+ evt.item.parentNode?.removeChild(evt.item)
99
+ requestAnimationFrame(() => {
100
+ options.onReorder(oldIdx, newIdx)
101
+ runCleanup(evt.item)
102
+ })
103
+ } else {
104
+ requestAnimationFrame(() => {
105
+ runCleanup(evt.item)
106
+ })
107
+ }
108
+ },
109
+ onAdd: (evt) => {
110
+ runCleanup(evt.item)
111
+ const itemEl = evt.item
112
+ const blockId = itemEl.dataset.id || itemEl.getAttribute("data-id")
113
+ if (blockId && evt.newIndex !== undefined && options.onAdd) {
114
+ const target = evt.from.closest("[data-block-id]")
115
+ const containerBlockId = target instanceof HTMLElement ? target.dataset.blockId : undefined
116
+ // Remove the physically-moved DOM node before calling the callback.
117
+ // The reactive framework (SolidJS) will re-render the authoritative element
118
+ // from state, so leaving the SortableJS-placed node causes a visual duplicate.
119
+ itemEl.parentNode?.removeChild(itemEl)
120
+ options.onAdd(blockId, evt.newIndex, containerBlockId, itemEl)
121
+ }
122
+ },
123
+ onRemove: (evt) => {
124
+ runCleanup(evt.item)
125
+ const itemEl = evt.item
126
+ const blockId = itemEl.dataset.id || itemEl.getAttribute("data-id")
127
+ if (blockId && evt.oldIndex !== undefined && options.onRemove) {
128
+ options.onRemove(blockId, evt.oldIndex)
129
+ }
130
+ }
131
+ })
132
+ }
133
+
134
+ function cleanElement(el: Element) {
135
+ if (el instanceof HTMLElement) {
136
+ el.classList.remove(
137
+ "cms-sortable-ghost",
138
+ "cms-sortable-chosen",
139
+ "cms-sortable-drag",
140
+ "sortable-fallback"
141
+ )
142
+ el.style.pointerEvents = ""
143
+ el.style.opacity = ""
144
+ el.style.transform = ""
145
+ el.style.boxShadow = ""
146
+ el.style.borderColor = ""
147
+ el.style.backgroundColor = ""
148
+ }
149
+ }
150
+
151
+ export function cleanAllSortableClasses(specificItem?: HTMLElement | Element) {
152
+ if (specificItem) {
153
+ cleanElement(specificItem)
154
+ specificItem.querySelectorAll("*").forEach(cleanElement)
155
+ }
156
+
157
+ if (typeof document !== "undefined") {
158
+ document
159
+ .querySelectorAll(
160
+ ".cms-sortable-ghost, .cms-sortable-chosen, .cms-sortable-drag, .sortable-fallback"
161
+ )
162
+ .forEach((el) => {
163
+ cleanElement(el)
164
+ el.querySelectorAll("*").forEach(cleanElement)
165
+ })
166
+ }
167
+ }
168
+
169
+ const customWin: Record<string, any> | null = typeof window !== "undefined" ? window : null
170
+ if (customWin && !customWin.cmsSortableGlobalListenersAttached) {
171
+ customWin.cmsSortableGlobalListenersAttached = true
172
+ const globalClean = () => {
173
+ const doClean = () => cleanAllSortableClasses()
174
+ doClean()
175
+ setTimeout(doClean, 50)
176
+ setTimeout(doClean, 150)
177
+ setTimeout(doClean, 300)
178
+ }
179
+ customWin.addEventListener("mouseup", globalClean, { capture: true })
180
+ customWin.addEventListener("pointerup", globalClean, { capture: true })
181
+ customWin.addEventListener("touchend", globalClean, { capture: true })
182
+ customWin.addEventListener("dragend", globalClean, { capture: true })
183
+ customWin.addEventListener("drop", globalClean, { capture: true })
184
+ }
@@ -0,0 +1,87 @@
1
+ const SNAP_POINTS = [25, 33.333, 50, 66.667, 75]
2
+ const SNAP_THRESHOLD = 2.0
3
+
4
+ export function initSplitPane(
5
+ container: HTMLElement,
6
+ leftPane: HTMLElement,
7
+ rightPane: HTMLElement,
8
+ handle: HTMLElement,
9
+ onResize?: (leftPct: number) => void
10
+ ) {
11
+ let isDragging = false
12
+ let animationFrameId: number | null = null
13
+ let pendingPercentage: number | null = null
14
+
15
+ const applyResize = () => {
16
+ if (pendingPercentage === null) return
17
+ leftPane.style.flex = `0 0 ${pendingPercentage}%`
18
+ leftPane.style.width = `${pendingPercentage}%`
19
+ rightPane.style.flex = `0 0 ${100 - pendingPercentage}%`
20
+ rightPane.style.width = `${100 - pendingPercentage}%`
21
+ onResize?.(pendingPercentage)
22
+ animationFrameId = null
23
+ }
24
+
25
+ const onMouseDown = (e: MouseEvent) => {
26
+ isDragging = true
27
+ handle.classList.add("active")
28
+ container.classList.add("split-dragging")
29
+ leftPane.style.transition = "none"
30
+ rightPane.style.transition = "none"
31
+ document.body.style.cursor = "col-resize"
32
+ document.body.style.userSelect = "none"
33
+ e.preventDefault()
34
+ }
35
+
36
+ const onMouseMove = (e: MouseEvent) => {
37
+ if (!isDragging) return
38
+ const containerRect = container.getBoundingClientRect()
39
+ const offset = e.clientX - containerRect.left
40
+ let percentage = (offset / containerRect.width) * 100
41
+
42
+ // Multi-point snapping (25%, 33.33%, 50%, 66.67%, 75%)
43
+ for (const snap of SNAP_POINTS) {
44
+ if (Math.abs(percentage - snap) <= SNAP_THRESHOLD) {
45
+ percentage = snap
46
+ break
47
+ }
48
+ }
49
+
50
+ // Keep within bounds 20% - 80%
51
+ percentage = Math.max(20, Math.min(80, percentage))
52
+
53
+ pendingPercentage = percentage
54
+ if (!animationFrameId) {
55
+ animationFrameId = requestAnimationFrame(applyResize)
56
+ }
57
+ }
58
+
59
+ const onMouseUp = () => {
60
+ if (isDragging) {
61
+ isDragging = false
62
+ handle.classList.remove("active")
63
+ container.classList.remove("split-dragging")
64
+ leftPane.style.transition = ""
65
+ rightPane.style.transition = ""
66
+ document.body.style.cursor = ""
67
+ document.body.style.userSelect = ""
68
+ if (animationFrameId) {
69
+ cancelAnimationFrame(animationFrameId)
70
+ animationFrameId = null
71
+ }
72
+ }
73
+ }
74
+
75
+ handle.addEventListener("mousedown", onMouseDown)
76
+ document.addEventListener("mousemove", onMouseMove)
77
+ document.addEventListener("mouseup", onMouseUp)
78
+
79
+ return () => {
80
+ handle.removeEventListener("mousedown", onMouseDown)
81
+ document.removeEventListener("mousemove", onMouseMove)
82
+ document.removeEventListener("mouseup", onMouseUp)
83
+ if (animationFrameId) {
84
+ cancelAnimationFrame(animationFrameId)
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,33 @@
1
+ import type { BaseBlock } from "../../core/types/blocks"
2
+
3
+ export function getBlockChildren(b: BaseBlock): BaseBlock[] {
4
+ if (!b) return []
5
+ if (Array.isArray(b.props?.children)) return b.props.children
6
+ if (Array.isArray(b.children)) return b.children
7
+ return []
8
+ }
9
+
10
+ export function sanitizeBlockTree(blocks: BaseBlock[]): BaseBlock[] {
11
+ if (!Array.isArray(blocks)) return []
12
+
13
+ const childIds = new Set<string>()
14
+
15
+ for (const b of blocks) {
16
+ if (!b) continue
17
+ const children = getBlockChildren(b)
18
+ if (Array.isArray(children) && children.length > 0) {
19
+ const collect = (list: BaseBlock[]) => {
20
+ for (const child of list) {
21
+ if (child && child.id) {
22
+ childIds.add(child.id)
23
+ const sub = getBlockChildren(child)
24
+ if (Array.isArray(sub) && sub.length > 0) collect(sub)
25
+ }
26
+ }
27
+ }
28
+ collect(children)
29
+ }
30
+ }
31
+
32
+ return blocks.filter((b) => b && b.id && !childIds.has(b.id))
33
+ }