diff --git a/lib/host/conformance.sh b/lib/host/conformance.sh index 15060561..cd258a5a 100755 --- a/lib/host/conformance.sh +++ b/lib/host/conformance.sh @@ -89,6 +89,7 @@ MODULES=( "lib/host/router.sx" "lib/host/static.sx" "lib/host/sx/relate-picker.sx" + "lib/host/sx/kg-cards.sx" "lib/host/feed.sx" "lib/host/relations.sx" "lib/host/blog.sx" diff --git a/lib/host/serve.sh b/lib/host/serve.sh index 7a3633b0..3184322d 100755 --- a/lib/host/serve.sh +++ b/lib/host/serve.sh @@ -83,6 +83,7 @@ MODULES=( "lib/host/router.sx" "lib/host/static.sx" "lib/host/sx/relate-picker.sx" + "lib/host/sx/kg-cards.sx" "lib/host/feed.sx" "lib/host/relations.sx" "lib/host/blog.sx" diff --git a/lib/host/sx/kg-cards.sx b/lib/host/sx/kg-cards.sx new file mode 100644 index 00000000..d988ddfb --- /dev/null +++ b/lib/host/sx/kg-cards.sx @@ -0,0 +1,157 @@ +;; KG card components — Ghost/Koenig-compatible card rendering, copied into the host +;; so it can render imported Ghost posts (sx_content holds (~kg_cards/kg-*) from the +;; lexical_to_sx converter). Produces the same HTML structure as lexical_renderer.py. +;; +;; ~rich-text: the host-local dep these cards need (raw HTML injection). Defined here +;; (it was only a test fixture before) so kg-html/kg-bookmark/etc. resolve in the host. +(defcomp ~rich-text (&key (html :as string)) (raw! html)) + +;; @css kg-card kg-image-card kg-width-wide kg-width-full kg-gallery-card kg-gallery-container kg-gallery-row kg-gallery-image kg-embed-card kg-bookmark-card kg-bookmark-container kg-bookmark-content kg-bookmark-title kg-bookmark-description kg-bookmark-metadata kg-bookmark-icon kg-bookmark-author kg-bookmark-publisher kg-bookmark-thumbnail kg-callout-card kg-callout-emoji kg-callout-text kg-button-card kg-btn kg-btn-accent kg-toggle-card kg-toggle-heading kg-toggle-heading-text kg-toggle-card-icon kg-toggle-content kg-audio-card kg-audio-thumbnail kg-audio-player-container kg-audio-title kg-audio-player kg-audio-play-icon kg-audio-current-time kg-audio-time kg-audio-seek-slider kg-audio-playback-rate kg-audio-unmute-icon kg-audio-volume-slider kg-video-card kg-video-container kg-file-card kg-file-card-container kg-file-card-contents kg-file-card-title kg-file-card-filesize kg-file-card-icon kg-file-card-caption kg-align-center kg-align-left kg-callout-card-grey kg-callout-card-white kg-callout-card-blue kg-callout-card-green kg-callout-card-yellow kg-callout-card-red kg-callout-card-pink kg-callout-card-purple kg-callout-card-accent kg-html-card kg-md-card placeholder + +;; --------------------------------------------------------------------------- +;; Image card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-image (&key (src :as string) (alt :as string?) (caption :as string?) (width :as string?) (href :as string?)) + (figure :class (str "kg-card kg-image-card" + (if (= width "wide") " kg-width-wide" + (if (= width "full") " kg-width-full" ""))) + (if href + (a :href href (img :src src :alt (or alt "") :loading "lazy")) + (img :src src :alt (or alt "") :loading "lazy")) + (when caption (figcaption caption)))) + +;; --------------------------------------------------------------------------- +;; Gallery card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-gallery (&key (images :as list) (caption :as string?)) + (figure :class "kg-card kg-gallery-card kg-width-wide" + (div :class "kg-gallery-container" + (map (lambda (row) + (div :class "kg-gallery-row" + (map (lambda (img-data) + (figure :class "kg-gallery-image" + (img :src (get img-data "src") :alt (or (get img-data "alt") "") :loading "lazy") + (when (get img-data "caption") (figcaption (get img-data "caption"))))) + row))) + images)) + (when caption (figcaption caption)))) + +;; --------------------------------------------------------------------------- +;; HTML card — wraps user-pasted HTML so the editor can identify the block. +;; Content is native sx children (no longer an opaque HTML string). +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-html (&rest children) + (div :class "kg-card kg-html-card" children)) + +;; --------------------------------------------------------------------------- +;; Markdown card — rendered markdown content, editor can identify the block. +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-md (&rest children) + (div :class "kg-card kg-md-card" children)) + +;; --------------------------------------------------------------------------- +;; Embed card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-embed (&key (html :as string) (caption :as string?)) + (figure :class "kg-card kg-embed-card" + (~rich-text :html html) + (when caption (figcaption caption)))) + +;; --------------------------------------------------------------------------- +;; Bookmark card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-bookmark (&key (url :as string) (title :as string?) (description :as string?) (icon :as string?) (author :as string?) (publisher :as string?) (thumbnail :as string?) (caption :as string?)) + (figure :class "kg-card kg-bookmark-card" + (a :class "kg-bookmark-container" :href url + (div :class "kg-bookmark-content" + (div :class "kg-bookmark-title" (or title "")) + (div :class "kg-bookmark-description" (or description "")) + (when (or icon author publisher) + (span :class "kg-bookmark-metadata" + (when icon (img :class "kg-bookmark-icon" :src icon :alt "")) + (when author (span :class "kg-bookmark-author" author)) + (when publisher (span :class "kg-bookmark-publisher" publisher))))) + (when thumbnail + (div :class "kg-bookmark-thumbnail" + (img :src thumbnail :alt "")))) + (when caption (figcaption caption)))) + +;; --------------------------------------------------------------------------- +;; Callout card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-callout (&key (color :as string?) (emoji :as string?) (content :as string?)) + (div :class (str "kg-card kg-callout-card kg-callout-card-" (or color "grey")) + (when emoji (div :class "kg-callout-emoji" emoji)) + (div :class "kg-callout-text" (or content "")))) + +;; --------------------------------------------------------------------------- +;; Button card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-button (&key (url :as string) (text :as string?) (alignment :as string?)) + (div :class (str "kg-card kg-button-card kg-align-" (or alignment "center")) + (a :href url :class "kg-btn kg-btn-accent" (or text "")))) + +;; --------------------------------------------------------------------------- +;; Toggle card (accordion) +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-toggle (&key (heading :as string?) (content :as string?)) + (div :class "kg-card kg-toggle-card" :data-kg-toggle-state "close" + (div :class "kg-toggle-heading" + (h4 :class "kg-toggle-heading-text" (or heading "")) + (button :class "kg-toggle-card-icon" + (~rich-text :html ""))) + (div :class "kg-toggle-content" (or content "")))) + +;; --------------------------------------------------------------------------- +;; Audio card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-audio (&key (src :as string) (title :as string?) (duration :as string?) (thumbnail :as string?)) + (div :class "kg-card kg-audio-card" + (if thumbnail + (img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail") + (div :class "kg-audio-thumbnail placeholder" + (~rich-text :html ""))) + (div :class "kg-audio-player-container" + (div :class "kg-audio-title" (or title "")) + (div :class "kg-audio-player" + (button :class "kg-audio-play-icon" + (~rich-text :html "")) + (div :class "kg-audio-current-time" "0:00") + (div :class "kg-audio-time" (str "/ " (or duration "0:00"))) + (input :type "range" :class "kg-audio-seek-slider" :max "100" :value "0") + (button :class "kg-audio-playback-rate" "1×") + (button :class "kg-audio-unmute-icon" + (~rich-text :html "")) + (input :type "range" :class "kg-audio-volume-slider" :max "100" :value "100"))) + (audio :src src :preload "metadata"))) + +;; --------------------------------------------------------------------------- +;; Video card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-video (&key (src :as string) (caption :as string?) (width :as string?) (thumbnail :as string?) (loop :as boolean?)) + (figure :class (str "kg-card kg-video-card" + (if (= width "wide") " kg-width-wide" + (if (= width "full") " kg-width-full" ""))) + (div :class "kg-video-container" + (video :src src :controls true :preload "metadata" + :poster (or thumbnail nil) :loop (or loop nil))) + (when caption (figcaption caption)))) + +;; --------------------------------------------------------------------------- +;; File card +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-file (&key (src :as string) (filename :as string?) (title :as string?) (filesize :as string?) (caption :as string?)) + (div :class "kg-card kg-file-card" + (a :class "kg-file-card-container" :href src :download (or filename "") + (div :class "kg-file-card-contents" + (div :class "kg-file-card-title" (or title filename "")) + (when filesize (div :class "kg-file-card-filesize" filesize))) + (div :class "kg-file-card-icon" + (~rich-text :html ""))) + (when caption (div :class "kg-file-card-caption" caption)))) + +;; --------------------------------------------------------------------------- +;; Paywall marker +;; --------------------------------------------------------------------------- +(defcomp ~kg_cards/kg-paywall () + (~rich-text :html ""))