dialog-release-notes.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { createSignal } from "solid-js"
  2. import { Dialog } from "@opencode-ai/ui/dialog"
  3. import { Button } from "@opencode-ai/ui/button"
  4. import { useDialog } from "@opencode-ai/ui/context/dialog"
  5. import { useLanguage } from "@/context/language"
  6. import { useSettings } from "@/context/settings"
  7. export type Highlight = {
  8. title: string
  9. description: string
  10. media?: {
  11. type: "image" | "video"
  12. src: string
  13. alt?: string
  14. }
  15. }
  16. export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
  17. const dialog = useDialog()
  18. const language = useLanguage()
  19. const settings = useSettings()
  20. const [index, setIndex] = createSignal(0)
  21. const total = () => props.highlights.length
  22. const last = () => Math.max(0, total() - 1)
  23. const feature = () => props.highlights[index()] ?? props.highlights[last()]
  24. const isFirst = () => index() === 0
  25. const isLast = () => index() >= last()
  26. const paged = () => total() > 1
  27. function handleNext() {
  28. if (isLast()) return
  29. setIndex(index() + 1)
  30. }
  31. function handleClose() {
  32. dialog.close()
  33. }
  34. function handleDisable() {
  35. settings.general.setReleaseNotes(false)
  36. handleClose()
  37. }
  38. function handleKeyDown(e: KeyboardEvent) {
  39. if (e.key === "Escape") {
  40. e.preventDefault()
  41. handleClose()
  42. return
  43. }
  44. if (!paged()) return
  45. if (e.key === "ArrowLeft" && !isFirst()) {
  46. e.preventDefault()
  47. setIndex(index() - 1)
  48. }
  49. if (e.key === "ArrowRight" && !isLast()) {
  50. e.preventDefault()
  51. setIndex(index() + 1)
  52. }
  53. }
  54. return (
  55. <Dialog
  56. size="large"
  57. fit
  58. class="w-[min(calc(100vw-40px),720px)] h-[min(calc(100vh-40px),400px)] -mt-20 min-h-0 overflow-hidden"
  59. >
  60. <div class="flex flex-1 min-w-0 min-h-0" tabIndex={0} autofocus onKeyDown={handleKeyDown}>
  61. {/* Left side - Text content */}
  62. <div class="flex flex-col flex-1 min-w-0 p-8">
  63. {/* Top section - feature content (fixed position from top) */}
  64. <div class="flex flex-col gap-2 pt-22">
  65. <div class="flex items-center gap-2">
  66. <h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
  67. </div>
  68. <p class="text-14-regular text-text-base">{feature()?.description ?? ""}</p>
  69. </div>
  70. {/* Spacer to push buttons to bottom */}
  71. <div class="flex-1" />
  72. {/* Bottom section - buttons and indicators (fixed position) */}
  73. <div class="flex flex-col gap-12">
  74. <div class="flex flex-col items-start gap-3">
  75. {isLast() ? (
  76. <Button variant="primary" size="large" onClick={handleClose}>
  77. {language.t("dialog.releaseNotes.action.getStarted")}
  78. </Button>
  79. ) : (
  80. <Button variant="secondary" size="large" onClick={handleNext}>
  81. {language.t("dialog.releaseNotes.action.next")}
  82. </Button>
  83. )}
  84. <Button variant="ghost" size="small" onClick={handleDisable}>
  85. {language.t("dialog.releaseNotes.action.hideFuture")}
  86. </Button>
  87. </div>
  88. {paged() && (
  89. <div class="flex items-center gap-1.5 -my-2.5">
  90. {props.highlights.map((_, i) => (
  91. <button
  92. type="button"
  93. class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
  94. classList={{
  95. "w-8": i === index(),
  96. "w-3": i !== index(),
  97. }}
  98. onClick={() => setIndex(i)}
  99. >
  100. <div
  101. class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
  102. classList={{
  103. "bg-icon-strong-base": i === index(),
  104. "bg-icon-weak-base": i !== index(),
  105. }}
  106. />
  107. </button>
  108. ))}
  109. </div>
  110. )}
  111. </div>
  112. </div>
  113. {/* Right side - Media content (edge to edge) */}
  114. {feature()?.media && (
  115. <div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
  116. {feature()!.media!.type === "image" ? (
  117. <img
  118. src={feature()!.media!.src}
  119. alt={feature()!.media!.alt ?? feature()?.title ?? language.t("dialog.releaseNotes.media.alt")}
  120. class="w-full h-full object-cover"
  121. />
  122. ) : (
  123. <video src={feature()!.media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
  124. )}
  125. </div>
  126. )}
  127. </div>
  128. </Dialog>
  129. )
  130. }