@rimelight/cms 0.0.1 → 0.0.3

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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +16 -14
  3. package/src/admin/api/media-file.ts +39 -0
  4. package/src/admin/api/media.ts +199 -0
  5. package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
  6. package/src/admin/layouts/DefaultParentLayout.astro +50 -0
  7. package/src/admin/pages/assets.astro +424 -0
  8. package/src/admin/pages/byline-schema.astro +62 -0
  9. package/src/admin/pages/bylines.astro +16 -0
  10. package/src/admin/pages/categories.astro +21 -0
  11. package/src/admin/pages/content-types.astro +55 -0
  12. package/src/admin/pages/dashboard.astro +61 -0
  13. package/src/admin/pages/pages-edit.astro +595 -0
  14. package/src/admin/pages/pages-index.astro +146 -0
  15. package/src/admin/pages/pages-new.astro +130 -0
  16. package/src/admin/pages/pages-preview.astro +111 -0
  17. package/src/admin/pages/pages-review.astro +236 -0
  18. package/src/admin/pages/settings.astro +203 -0
  19. package/src/admin/pages/tags.astro +21 -0
  20. package/src/admin/pages/templates-edit.astro +234 -0
  21. package/src/admin/pages/templates-index.astro +95 -0
  22. package/src/admin/pages/templates-new.astro +193 -0
  23. package/src/admin/pages/users.astro +74 -0
  24. package/src/admin/pages/versions.astro +74 -0
  25. package/src/astro/BlockRenderer.astro +44 -96
  26. package/src/astro/PageRenderer.astro +9 -2
  27. package/src/astro/blocks/CalloutBlock.astro +7 -7
  28. package/src/astro/blocks/CardBlock.astro +5 -5
  29. package/src/astro/blocks/CodeBlock.astro +2 -2
  30. package/src/astro/blocks/FileTreeBlock.astro +7 -7
  31. package/src/astro/blocks/ImageBlock.astro +2 -2
  32. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  33. package/src/astro/blocks/ParagraphBlock.astro +3 -3
  34. package/src/astro/blocks/SectionBlock.astro +6 -6
  35. package/src/astro/blocks/StepItemBlock.astro +3 -3
  36. package/src/astro/blocks/StepsBlock.astro +1 -1
  37. package/src/astro/blocks/TableBlock.astro +6 -6
  38. package/src/astro/blocks/TabsBlock.astro +9 -9
  39. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  40. package/src/astro/blocks/index.ts +35 -0
  41. package/src/client/components/RimelightBlock.tsx +19 -19
  42. package/src/client/components/RimelightBlockPicker.tsx +9 -13
  43. package/src/client/components/RimelightBylinesManager.tsx +438 -0
  44. package/src/client/components/RimelightEditor.tsx +3 -3
  45. package/src/client/components/RimelightPreview.tsx +37 -45
  46. package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
  47. package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
  48. package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
  49. package/src/client/components/RimelightTemplateEditor.tsx +6 -8
  50. package/src/client/components/editors/CalloutEditor.tsx +2 -4
  51. package/src/client/components/editors/CardsEditor.tsx +2 -4
  52. package/src/client/components/editors/CodeEditor.tsx +1 -3
  53. package/src/client/components/editors/FileTreeEditor.tsx +2 -4
  54. package/src/client/components/editors/ImageEditor.tsx +3 -3
  55. package/src/client/components/editors/ParagraphEditor.tsx +2 -2
  56. package/src/client/components/editors/SceneEditor.tsx +6 -12
  57. package/src/client/components/editors/ScriptEditor.tsx +8 -16
  58. package/src/client/components/editors/SectionEditor.tsx +4 -4
  59. package/src/client/components/editors/StepItemEditor.tsx +2 -2
  60. package/src/client/components/editors/StepsEditor.tsx +2 -4
  61. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  62. package/src/client/components/editors/TableEditor.tsx +5 -5
  63. package/src/client/components/editors/TabsEditor.tsx +2 -4
  64. package/src/client/components/index.ts +3 -0
  65. package/src/client/loader.ts +3 -0
  66. package/src/core/diff.ts +145 -0
  67. package/src/core/excerpt.ts +159 -0
  68. package/src/core/preview.ts +87 -0
  69. package/src/core/registry.ts +0 -39
  70. package/src/core/types/blocks.ts +83 -6
  71. package/src/core/types/bylines.ts +14 -0
  72. package/src/core/types/media.ts +13 -0
  73. package/src/core/types/taxonomies.ts +20 -0
  74. package/src/cron/scheduled.ts +57 -0
  75. package/src/docs/agent.ts +116 -0
  76. package/src/docs/config.ts +25 -0
  77. package/src/docs/index.ts +5 -0
  78. package/src/docs/routing.ts +104 -0
  79. package/src/docs/sidebar.ts +251 -0
  80. package/src/docs/toc.ts +47 -0
  81. package/src/env.d.ts +13 -0
  82. package/src/index.ts +40 -6
  83. package/src/integration.ts +196 -0
  84. package/src/loader/live.ts +254 -0
  85. package/src/markdown/index.ts +0 -1
  86. package/src/mcp/index.ts +228 -0
  87. package/src/schema/bylines.ts +23 -0
  88. package/src/schema/index.ts +3 -0
  89. package/src/schema/page_versions.ts +2 -0
  90. package/src/schema/pages.ts +2 -0
  91. package/src/schema/site_settings.ts +41 -0
  92. package/src/schema/taxonomies.ts +49 -0
  93. package/src/services/bylines.ts +74 -0
  94. package/src/services/search-indexer.ts +61 -0
  95. package/src/services/site-settings.ts +108 -0
  96. package/src/services/taxonomies.ts +276 -0
  97. package/src/storage/index.ts +253 -0
  98. package/src/astro/blocks/DialogueBlock.astro +0 -23
  99. package/src/astro/blocks/SceneBlock.astro +0 -65
  100. package/src/astro/blocks/ScriptBlock.astro +0 -59
  101. package/src/core/template-sync.ts +0 -75
  102. package/src/markdown/parser.ts +0 -347
  103. package/src/migration.ts +0 -155
  104. package/src/search/sync.ts +0 -32
@@ -3,7 +3,7 @@ import {
3
3
  getPageDefinition,
4
4
  type PageDefinition,
5
5
  type FieldDefinition
6
- } from "../../core/page-definitions"
6
+ } from "../../core/page-definitions.ts"
7
7
  import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
8
8
  import RLSSelect from "@rimelight/ui/components/select/RLSSelect.tsx"
9
9
  import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
@@ -18,6 +18,13 @@ function getTextValue(value: any): string {
18
18
  return ""
19
19
  }
20
20
 
21
+ export interface BylineItem {
22
+ name: string
23
+ avatar?: string
24
+ role?: string
25
+ url?: string
26
+ }
27
+
21
28
  export interface RimelightPropertiesPanelProps {
22
29
  pageType?: string
23
30
  initialProperties?: Record<string, any>
@@ -25,6 +32,7 @@ export interface RimelightPropertiesPanelProps {
25
32
  slug?: string
26
33
  icon?: string
27
34
  tags?: string[]
35
+ bylines?: BylineItem[]
28
36
  onPropertiesChange?: (props: Record<string, any>) => void
29
37
  class?: string
30
38
  }
@@ -44,11 +52,37 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
44
52
  componentProps.initialProperties?.links || []
45
53
  )
46
54
 
55
+ // Bylines state
56
+ const [availableBylines, setAvailableBylines] = createSignal<BylineItem[]>([])
57
+ const [bylines, setBylines] = createSignal<BylineItem[]>(
58
+ componentProps.bylines || componentProps.initialProperties?.bylines || []
59
+ )
60
+ const [isBylineModalOpen, setIsBylineModalOpen] = createSignal(false)
61
+ const [editingBylineIndex, setEditingBylineIndex] = createSignal<number | null>(null)
62
+ const [bylineDraft, setBylineDraft] = createSignal<BylineItem>({
63
+ name: "",
64
+ avatar: "",
65
+ role: "",
66
+ url: ""
67
+ })
68
+
69
+ // Taxonomies state
70
+ const [categoriesList, setCategoriesList] = createSignal<
71
+ Array<{ id: string; name: string; slug: string }>
72
+ >([])
73
+ const [selectedCategory, setSelectedCategory] = createSignal<string>(
74
+ componentProps.initialProperties?.category || ""
75
+ )
76
+ const [tagsList, setTagsList] = createSignal<string[]>(
77
+ componentProps.tags || componentProps.initialProperties?.tags || []
78
+ )
79
+ const [newTagInput, setNewTagInput] = createSignal("")
80
+
47
81
  const [isLinkModalOpen, setIsLinkModalOpen] = createSignal(false)
48
82
  const [editingLinkIndex, setEditingLinkIndex] = createSignal<number | null>(null)
49
83
  const [linkDraft, setLinkDraft] = createSignal({ label: "", to: "", icon: "" })
50
84
 
51
- onMount(() => {
85
+ onMount(async () => {
52
86
  const def = definition()
53
87
  if (def?.properties) {
54
88
  const initialExpanded: Record<string, boolean> = {}
@@ -61,9 +95,45 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
61
95
  const handlePropsChange = (e: Event) => {
62
96
  if (e instanceof CustomEvent && e.detail?.properties) {
63
97
  setProperties(e.detail.properties)
98
+ if (e.detail.properties.bylines) setBylines(e.detail.properties.bylines)
99
+ if (e.detail.properties.tags) setTagsList(e.detail.properties.tags)
100
+ if (e.detail.properties.category) setSelectedCategory(e.detail.properties.category)
64
101
  }
65
102
  }
66
103
  document.addEventListener("properties-change", handlePropsChange)
104
+
105
+ // Load available taxonomy categories
106
+ try {
107
+ const res = await fetch("/api/cms/taxonomies/category/terms")
108
+ if (res.ok) {
109
+ const data = await res.json()
110
+ if (Array.isArray(data)) {
111
+ setCategoriesList(data.map((t: any) => ({ id: t.id, name: t.name, slug: t.slug })))
112
+ }
113
+ }
114
+ } catch {
115
+ // ignore
116
+ }
117
+
118
+ // Load available global bylines
119
+ try {
120
+ const res = await fetch("/api/cms/bylines")
121
+ if (res.ok) {
122
+ const data = await res.json()
123
+ if (Array.isArray(data)) {
124
+ setAvailableBylines(
125
+ data.map((b: any) => ({
126
+ name: b.name,
127
+ avatar: b.avatar || "",
128
+ role: b.bio || "",
129
+ url: b.websiteUrl || ""
130
+ }))
131
+ )
132
+ }
133
+ }
134
+ } catch {
135
+ // ignore
136
+ }
67
137
  })
68
138
 
69
139
  const handlePropertyChange = (key: string, value: any) => {
@@ -84,6 +154,63 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
84
154
  setExpandedGroups((prev) => ({ ...prev, [groupId]: !prev[groupId] }))
85
155
  }
86
156
 
157
+ // Bylines Handlers
158
+ const openBylineModal = (index: number | null = null) => {
159
+ setEditingBylineIndex(index)
160
+ if (index !== null && bylines()[index]) {
161
+ const b = bylines()[index]!
162
+ setBylineDraft({ name: b.name, avatar: b.avatar || "", role: b.role || "", url: b.url || "" })
163
+ } else {
164
+ setBylineDraft({ name: "", avatar: "", role: "", url: "" })
165
+ }
166
+ setIsBylineModalOpen(true)
167
+ }
168
+
169
+ const saveByline = () => {
170
+ const draft = bylineDraft()
171
+ if (!draft.name) return
172
+ const current = [...bylines()]
173
+ const idx = editingBylineIndex()
174
+ if (idx !== null) {
175
+ current[idx] = draft
176
+ } else {
177
+ current.push(draft)
178
+ }
179
+ setBylines(current)
180
+ handlePropertyChange("bylines", current)
181
+ setIsBylineModalOpen(false)
182
+ }
183
+
184
+ const removeByline = (index: number) => {
185
+ const current = [...bylines()]
186
+ current.splice(index, 1)
187
+ setBylines(current)
188
+ handlePropertyChange("bylines", current)
189
+ }
190
+
191
+ // Taxonomies Handlers
192
+ const handleCategoryChange = (slug: string) => {
193
+ setSelectedCategory(slug)
194
+ handlePropertyChange("category", slug)
195
+ }
196
+
197
+ const addTag = () => {
198
+ const tag = newTagInput().trim().toLowerCase()
199
+ if (tag && !tagsList().includes(tag)) {
200
+ const updated = [...tagsList(), tag]
201
+ setTagsList(updated)
202
+ handlePropertyChange("tags", updated)
203
+ setNewTagInput("")
204
+ }
205
+ }
206
+
207
+ const removeTag = (tagToRemove: string) => {
208
+ const updated = tagsList().filter((t) => t !== tagToRemove)
209
+ setTagsList(updated)
210
+ handlePropertyChange("tags", updated)
211
+ }
212
+
213
+ // Links Handlers
87
214
  const openLinkModal = (index: number | null = null) => {
88
215
  setEditingLinkIndex(index)
89
216
  if (index !== null && links()[index]) {
@@ -124,9 +251,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
124
251
  if (field.type === "text") {
125
252
  return (
126
253
  <div class="flex flex-col gap-1 w-full">
127
- <span class="text-xs font-semibold text-neutral-600 dark:text-neutral-400 block">
128
- {label}
129
- </span>
254
+ <span class="text-xs font-semibold text-neutral-600 block">{label}</span>
130
255
  <RLSInput
131
256
  value={getTextValue(value)}
132
257
  onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
@@ -142,9 +267,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
142
267
  if (field.type === "number") {
143
268
  return (
144
269
  <div class="flex flex-col gap-1 w-full">
145
- <span class="text-xs font-semibold text-neutral-600 dark:text-neutral-400 block">
146
- {label}
147
- </span>
270
+ <span class="text-xs font-semibold text-neutral-600 block">{label}</span>
148
271
  <RLSInput
149
272
  type="number"
150
273
  value={value !== undefined ? String(value) : ""}
@@ -162,9 +285,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
162
285
  const selectItems = (field.options || []).map((opt) => getTextValue(opt))
163
286
  return (
164
287
  <div class="flex flex-col gap-1 w-full">
165
- <span class="text-xs font-semibold text-neutral-600 dark:text-neutral-400 block">
166
- {label}
167
- </span>
288
+ <span class="text-xs font-semibold text-neutral-600 block">{label}</span>
168
289
  <RLSSelect
169
290
  items={selectItems}
170
291
  value={getTextValue(value)}
@@ -196,9 +317,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
196
317
 
197
318
  return (
198
319
  <div class="flex flex-col gap-1 w-full">
199
- <span class="text-xs font-semibold text-neutral-600 dark:text-neutral-400 block">
200
- {label}
201
- </span>
320
+ <span class="text-xs font-semibold text-neutral-600 block">{label}</span>
202
321
  <div class="flex flex-wrap gap-1 mb-1">
203
322
  <For each={arr}>
204
323
  {(item, idx) => (
@@ -207,7 +326,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
207
326
  <button
208
327
  type="button"
209
328
  onClick={() => removeArrayItem(idx())}
210
- class="ml-1 text-muted-foreground hover:text-red-500 cursor-pointer bg-transparent border-0 p-0 inline-flex items-center"
329
+ class="ml-1 text-muted hover:text-error-500 cursor-pointer bg-transparent border-0 p-0 inline-flex items-center"
211
330
  >
212
331
  ×
213
332
  </button>
@@ -253,16 +372,168 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
253
372
  {/* Top header content */}
254
373
  <div class="flex flex-col gap-2 items-center text-center">
255
374
  <div class="relative group/icon mb-1">
256
- <div class="w-10 h-10 rounded-full flex items-center justify-center border border-default text-muted-foreground">
375
+ <div class="w-10 h-10 rounded-full flex items-center justify-center border border-default text-muted">
257
376
  <RLSIcon name="fileText" size="md" />
258
377
  </div>
259
378
  </div>
260
379
 
261
- <span class="text-xs text-dimmed uppercase tracking-wider text-center block font-semibold text-muted-foreground">
380
+ <span class="text-xs uppercase tracking-wider text-center block font-semibold text-muted">
262
381
  {pageType()} Properties
263
382
  </span>
264
383
  </div>
265
384
 
385
+ {/* Taxonomies Section */}
386
+ <div class="mt-4 p-3 rounded-lg border border-default/70 bg-elevated/30 flex flex-col gap-3">
387
+ <h6 class="text-xs font-bold uppercase tracking-wider text-muted">Taxonomies</h6>
388
+
389
+ {/* Category */}
390
+ <div class="flex flex-col gap-1 w-full">
391
+ <span class="text-xs font-semibold text-neutral-600 block">Category</span>
392
+ <select
393
+ value={selectedCategory()}
394
+ onChange={(e) => handleCategoryChange(e.currentTarget.value)}
395
+ class="w-full text-xs py-1.5 px-2.5 rounded-md border border-default bg-background text-highlighted focus:outline-none focus:ring-1 focus:ring-primary"
396
+ >
397
+ <option value="">(None)</option>
398
+ <For each={categoriesList()}>
399
+ {(cat) => (
400
+ <option value={cat.slug} selected={selectedCategory() === cat.slug}>
401
+ {cat.name}
402
+ </option>
403
+ )}
404
+ </For>
405
+ </select>
406
+ </div>
407
+
408
+ {/* Tags */}
409
+ <div class="flex flex-col gap-1 w-full">
410
+ <span class="text-xs font-semibold text-neutral-600 block">Tags</span>
411
+ <div class="flex flex-wrap gap-1 mb-1">
412
+ <For each={tagsList()}>
413
+ {(tag) => (
414
+ <RLSBadge variant="soft" color="primary" size="sm">
415
+ <span>#{tag}</span>
416
+ <button
417
+ type="button"
418
+ onClick={() => removeTag(tag)}
419
+ class="ml-1 text-muted hover:text-error-500 cursor-pointer bg-transparent border-0 p-0 inline-flex items-center"
420
+ >
421
+ ×
422
+ </button>
423
+ </RLSBadge>
424
+ )}
425
+ </For>
426
+ </div>
427
+ <div class="flex gap-1 mt-1">
428
+ <div class="flex-1">
429
+ <RLSInput
430
+ value={newTagInput()}
431
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
432
+ setNewTagInput(e.currentTarget.value)
433
+ }
434
+ onKeyDown={(e: KeyboardEvent) => {
435
+ if (e.key === "Enter") {
436
+ e.preventDefault()
437
+ addTag()
438
+ }
439
+ }}
440
+ size="sm"
441
+ placeholder="Add tag..."
442
+ />
443
+ </div>
444
+ <RLSButton variant="outline" color="neutral" size="sm" onClick={addTag} label="+" />
445
+ </div>
446
+ </div>
447
+ </div>
448
+
449
+ {/* Bylines / Authors Section */}
450
+ <div class="mt-3 p-3 rounded-lg border border-default/70 bg-elevated/30 flex flex-col gap-2">
451
+ <div class="flex items-center justify-between mb-1">
452
+ <h6 class="text-xs font-bold uppercase tracking-wider text-muted">Bylines / Authors</h6>
453
+ <RLSButton
454
+ variant="ghost"
455
+ color="neutral"
456
+ size="xs"
457
+ onClick={() => openBylineModal()}
458
+ leadingIcon="plus"
459
+ label="Add Custom"
460
+ />
461
+ </div>
462
+
463
+ <Show when={availableBylines().length > 0}>
464
+ <select
465
+ onChange={(e) => {
466
+ const authorName = e.currentTarget.value
467
+ if (!authorName) return
468
+ const found = availableBylines().find((b) => b.name === authorName)
469
+ if (found && !bylines().some((b) => b.name === found.name)) {
470
+ const updated = [...bylines(), found]
471
+ setBylines(updated)
472
+ handlePropertyChange("bylines", updated)
473
+ }
474
+ e.currentTarget.value = ""
475
+ }}
476
+ class="w-full text-xs py-1.5 px-2 rounded-md border border-default bg-background text-highlighted focus:outline-none focus:ring-1 focus:ring-primary mb-1"
477
+ >
478
+ <option value="">+ Select from global bylines...</option>
479
+ <For each={availableBylines()}>
480
+ {(author) => <option value={author.name}>{author.name}</option>}
481
+ </For>
482
+ </select>
483
+ </Show>
484
+
485
+ <Show
486
+ when={bylines().length > 0}
487
+ fallback={<p class="text-xs text-muted italic">No authors attached</p>}
488
+ >
489
+ <div class="flex flex-col gap-2">
490
+ <For each={bylines()}>
491
+ {(byline, idx) => (
492
+ <div class="flex items-center justify-between p-2 rounded-lg border border-default/50 text-xs bg-background/50">
493
+ <div class="flex items-center gap-2 truncate">
494
+ {byline.avatar ? (
495
+ <img
496
+ src={byline.avatar}
497
+ alt={byline.name}
498
+ class="w-6 h-6 rounded-full object-cover shrink-0"
499
+ />
500
+ ) : (
501
+ <div class="w-6 h-6 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold text-[10px] shrink-0">
502
+ {byline.name.charAt(0).toUpperCase()}
503
+ </div>
504
+ )}
505
+ <div class="flex flex-col truncate">
506
+ <span class="font-semibold text-highlighted truncate">{byline.name}</span>
507
+ {byline.role && (
508
+ <span class="text-[10px] text-muted truncate">{byline.role}</span>
509
+ )}
510
+ </div>
511
+ </div>
512
+ <div class="flex items-center gap-1 shrink-0">
513
+ <button
514
+ type="button"
515
+ onClick={() => openBylineModal(idx())}
516
+ class="text-muted hover:text-primary cursor-pointer p-1"
517
+ title="Edit"
518
+ >
519
+ <RLSIcon name="edit" size="xs" />
520
+ </button>
521
+ <button
522
+ type="button"
523
+ onClick={() => removeByline(idx())}
524
+ class="text-muted hover:text-error-500 cursor-pointer p-1"
525
+ title="Delete"
526
+ >
527
+ <RLSIcon name="trash" size="xs" />
528
+ </button>
529
+ </div>
530
+ </div>
531
+ )}
532
+ </For>
533
+ </div>
534
+ </Show>
535
+ </div>
536
+
266
537
  {/* Property Groups Accordion */}
267
538
  <div class="mt-4 space-y-2 w-full">
268
539
  <For each={Object.entries(definition()?.properties || {})}>
@@ -273,13 +544,13 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
273
544
  <button
274
545
  type="button"
275
546
  onClick={() => toggleGroup(groupId)}
276
- class="flex justify-between items-center w-full p-2 rounded-lg text-left font-medium text-sm text-foreground hover:bg-elevated transition cursor-pointer mt-2 bg-transparent border-0"
547
+ class="flex justify-between items-center w-full p-2 rounded-lg text-left font-medium text-sm text-highlighted hover:bg-elevated transition cursor-pointer mt-2 bg-transparent border-0"
277
548
  >
278
549
  <span>{getTextValue(group.label)}</span>
279
550
  <RLSIcon
280
551
  name="chevronDown"
281
552
  size="sm"
282
- class={`transition-transform duration-150 text-muted-foreground ${
553
+ class={`transition-transform duration-150 text-muted ${
283
554
  isExpanded() ? "rotate-180" : ""
284
555
  }`}
285
556
  />
@@ -302,7 +573,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
302
573
  {/* Links section */}
303
574
  <div class="flex flex-col gap-2 pt-2 border-t border-default">
304
575
  <div class="flex items-center justify-between mb-1">
305
- <h6 class="text-xs font-bold uppercase tracking-wider text-muted-foreground">Links</h6>
576
+ <h6 class="text-xs font-bold uppercase tracking-wider text-muted">Links</h6>
306
577
  <RLSButton
307
578
  variant="ghost"
308
579
  color="neutral"
@@ -315,15 +586,15 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
315
586
 
316
587
  <Show
317
588
  when={links().length > 0}
318
- fallback={<p class="text-xs text-muted-foreground italic">No links added</p>}
589
+ fallback={<p class="text-xs text-muted italic">No links added</p>}
319
590
  >
320
591
  <div class="flex flex-col gap-2">
321
592
  <For each={links()}>
322
593
  {(item, index) => (
323
594
  <div class="flex items-center justify-between p-2 rounded-lg border border-default text-sm">
324
595
  <div class="flex items-center gap-2 truncate">
325
- <RLSIcon name="link" size="xs" class="text-muted-foreground shrink-0" />
326
- <span class="font-medium text-foreground truncate">{item.label}</span>
596
+ <RLSIcon name="link" size="xs" class="text-muted shrink-0" />
597
+ <span class="font-medium text-highlighted truncate">{item.label}</span>
327
598
  </div>
328
599
  <div class="flex items-center gap-1 shrink-0">
329
600
  <RLSButton
@@ -344,7 +615,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
344
615
  onClick={() => removeLink(index())}
345
616
  aria-label="Delete"
346
617
  title="Delete"
347
- class="!text-red-500 hover:!text-red-600"
618
+ class="!text-error-500 hover:!text-error-600"
348
619
  leading={<RLSIcon name="trash" size="xs" />}
349
620
  />
350
621
  </div>
@@ -355,6 +626,77 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
355
626
  </Show>
356
627
  </div>
357
628
 
629
+ {/* Byline Modal */}
630
+ <RLSModal
631
+ open={isBylineModalOpen()}
632
+ onClose={() => setIsBylineModalOpen(false)}
633
+ title={editingBylineIndex() !== null ? "Edit Author Byline" : "Add Author Byline"}
634
+ footerSlot={
635
+ <>
636
+ <RLSButton
637
+ variant="outline"
638
+ color="neutral"
639
+ size="sm"
640
+ onClick={() => setIsBylineModalOpen(false)}
641
+ label="Cancel"
642
+ />
643
+ <RLSButton
644
+ variant="solid"
645
+ color="primary"
646
+ size="sm"
647
+ onClick={saveByline}
648
+ label={editingBylineIndex() !== null ? "Update" : "Add"}
649
+ />
650
+ </>
651
+ }
652
+ >
653
+ <div class="flex flex-col gap-4 py-2">
654
+ <div class="flex flex-col gap-1">
655
+ <span class="text-xs font-semibold text-muted">Author Name *</span>
656
+ <RLSInput
657
+ value={bylineDraft().name}
658
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
659
+ setBylineDraft({ ...bylineDraft(), name: e.currentTarget.value })
660
+ }
661
+ placeholder="e.g. Idan"
662
+ />
663
+ </div>
664
+
665
+ <div class="flex flex-col gap-1">
666
+ <span class="text-xs font-semibold text-muted">Role / Title</span>
667
+ <RLSInput
668
+ value={bylineDraft().role || ""}
669
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
670
+ setBylineDraft({ ...bylineDraft(), role: e.currentTarget.value })
671
+ }
672
+ placeholder="e.g. Lead Producer"
673
+ />
674
+ </div>
675
+
676
+ <div class="flex flex-col gap-1">
677
+ <span class="text-xs font-semibold text-muted">Avatar URL</span>
678
+ <RLSInput
679
+ value={bylineDraft().avatar || ""}
680
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
681
+ setBylineDraft({ ...bylineDraft(), avatar: e.currentTarget.value })
682
+ }
683
+ placeholder="https://..."
684
+ />
685
+ </div>
686
+
687
+ <div class="flex flex-col gap-1">
688
+ <span class="text-xs font-semibold text-muted">Profile / Website URL</span>
689
+ <RLSInput
690
+ value={bylineDraft().url || ""}
691
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
692
+ setBylineDraft({ ...bylineDraft(), url: e.currentTarget.value })
693
+ }
694
+ placeholder="https://rimelight.com/authors/..."
695
+ />
696
+ </div>
697
+ </div>
698
+ </RLSModal>
699
+
358
700
  {/* Link Add/Edit Modal */}
359
701
  <RLSModal
360
702
  open={isLinkModalOpen()}
@@ -381,7 +723,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
381
723
  >
382
724
  <div class="flex flex-col gap-4 py-2">
383
725
  <div class="flex flex-col gap-1">
384
- <span class="text-xs font-semibold text-muted-foreground">Label</span>
726
+ <span class="text-xs font-semibold text-muted">Label</span>
385
727
  <RLSInput
386
728
  value={linkDraft().label}
387
729
  onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
@@ -392,7 +734,7 @@ export const RimelightPropertiesPanel: Component<RimelightPropertiesPanelProps>
392
734
  </div>
393
735
 
394
736
  <div class="flex flex-col gap-1">
395
- <span class="text-xs font-semibold text-muted-foreground">URL</span>
737
+ <span class="text-xs font-semibold text-muted">URL</span>
396
738
  <RLSInput
397
739
  value={linkDraft().to}
398
740
  onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>