diff --git a/frontend/package.json b/frontend/package.json index 08198b3c6..89d7b1202 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "@sentry/nextjs": "^10.52.0", "allotment": "^1.20.3", "ansi-to-react": "^6.1.6", + "boarding.js": "^3.7.3", "clsx": "^2.1.1", "codemirror": "^6.0.1", "dotenv": "^16.4.5", diff --git a/frontend/src/app/globals.scss b/frontend/src/app/globals.scss index e9a4a6cbe..2b94d83c9 100644 --- a/frontend/src/app/globals.scss +++ b/frontend/src/app/globals.scss @@ -85,3 +85,127 @@ body.no-scroll { height: 100svh; } } + +.scratch-tour-click-target { + position: relative; + isolation: isolate; + border-radius: 6px; + animation: scratch-tour-click-target-pulse 1.2s ease-out infinite; +} + +@keyframes scratch-tour-click-target-pulse { + 0% { + box-shadow: 0 0 0 0 rgb(192 88 255 / 60%); + } + + 70% { + box-shadow: 0 0 0 10px rgb(192 88 255 / 0%); + } + + 100% { + box-shadow: 0 0 0 0 rgb(192 88 255 / 0%); + } +} + +@media (prefers-reduced-motion: reduce) { + .scratch-tour-click-target { + animation: none; + box-shadow: 0 0 0 3px rgb(192 88 255 / 60%); + } +} + +#boarding-popover-item.scratch-tour-popover { + width: min(22rem, calc(100vw - 2rem)); + border: 1px solid var(--g600); + border-radius: 8px; + background: var(--g200); + box-shadow: 0 18px 48px rgb(0 0 0 / 35%); + color: var(--g1700); + + .boarding-popover-title { + margin-bottom: 0.45rem; + font-weight: 700; + font-size: 0.95rem; + line-height: 1.2; + } + + .boarding-popover-description { + color: var(--g1200); + font-size: 0.875rem; + line-height: 1.35; + + strong { + color: var(--g1700); + font-weight: 700; + } + + a { + color: var(--link); + text-decoration: underline; + } + + a:hover { + color: var(--g1700); + } + + li { + margin-left: 0.5rem; + } + } + + .boarding-popover-footer { + display: flex !important; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-top: 1rem; + } + + .boarding-navigation-btns { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: auto; + } + + button { + display: inline-flex; + min-height: 2rem; + align-items: center; + justify-content: center; + border: 1px solid var(--g700); + border-radius: 6px; + background: var(--g400); + padding: 0.35rem 0.65rem; + color: var(--g1700); + font-weight: 600; + font-size: 0.8125rem; + line-height: 1; + white-space: nowrap; + cursor: pointer; + } + + button:hover { + background: var(--a600); + } + + .boarding-next-btn { + border-color: var(--a900); + background: var(--a400); + color: white; + } + + .boarding-next-btn:hover { + background: var(--a600); + } + + .boarding-disabled { + opacity: 0.45; + cursor: default; + pointer-events: none; + } + + .boarding-popover-tip { + border-color: var(--g200); + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 800b1f540..808bb503d 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,6 +1,7 @@ import ThemeProvider from "./ThemeProvider"; import "allotment/dist/style.css"; +import "boarding.js/styles/main.css"; import "./globals.scss"; export const metadata = { diff --git a/frontend/src/components/CustomLayout.tsx b/frontend/src/components/CustomLayout.tsx index 59d72d7ca..d3b2c33f7 100644 --- a/frontend/src/components/CustomLayout.tsx +++ b/frontend/src/components/CustomLayout.tsx @@ -89,11 +89,22 @@ export default function CustomLayout({ renderTab, layout, onChange }: Props) { els.push( - +
+ +
, ); } diff --git a/frontend/src/components/Diff/Diff.module.scss b/frontend/src/components/Diff/Diff.module.scss index 89f6c2c51..1c9f71b34 100644 --- a/frontend/src/components/Diff/Diff.module.scss +++ b/frontend/src/components/Diff/Diff.module.scss @@ -52,6 +52,29 @@ } } +.columnTourTarget { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + pointer-events: none; +} + +.columnTourTarget-base { + left: 0; + width: var(--diff-left-width); +} + +.columnTourTarget-current { + left: var(--diff-left-width); + right: var(--diff-right-width); +} + +.columnTourTarget-previous { + right: 0; + width: var(--diff-right-width); +} + .searchPanel { position: absolute; z-index: 120; @@ -247,6 +270,16 @@ .register { color: #aa8b00; } .symbol { color: #fff; } +.diffLegendToken { + display: inline-block; + min-width: 1.25ch; + margin-right: 0.35rem; + + font-family: var(--monospace); + font-weight: 700; + text-align: center; +} + .delay_slot { font-weight: bold; color: #969896; diff --git a/frontend/src/components/Diff/Diff.tsx b/frontend/src/components/Diff/Diff.tsx index 6ac30980c..bc36eceb6 100644 --- a/frontend/src/components/Diff/Diff.tsx +++ b/frontend/src/components/Diff/Diff.tsx @@ -11,6 +11,7 @@ import { } from "react"; import { FoldIcon, SearchIcon } from "@primer/octicons-react"; +import clsx from "clsx"; import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeList } from "react-window"; import { useDebounce } from "use-debounce"; @@ -479,9 +480,23 @@ export default function Diff({ > {columnCount >= 2 && } {columnCount === 3 && } + {columns.map((col) => ( +
+ ))}
{columns.map((col) => ( -
+
{COLUMN_LABELS[col]}
- - setColumn("base", enabled) - } - /> - - setColumn("current", enabled) - } - /> - - } - disabledLabel="Collapse streaks of " - enabledLabel="Show all " - title="matching lines" - padding="px-1 py-1" - enabled={compressionEnabled} - setEnabled={setCompressionEnabled} - /> +
+ + setColumn("base", enabled) + } + dataTour="scratch-diff-toggle-target" + /> + + setColumn("current", enabled) + } + dataTour="scratch-diff-toggle-current" + /> + + } + disabledLabel="Collapse streaks of " + enabledLabel="Show all " + title="matching lines" + padding="px-1 py-1" + enabled={compressionEnabled} + setEnabled={setCompressionEnabled} + dataTour="scratch-diff-toggle-compression" + /> +
)}
diff --git a/frontend/src/components/Diff/ToggleButton.tsx b/frontend/src/components/Diff/ToggleButton.tsx index 803f870aa..df87a9dba 100644 --- a/frontend/src/components/Diff/ToggleButton.tsx +++ b/frontend/src/components/Diff/ToggleButton.tsx @@ -9,6 +9,7 @@ type ToggleButtonProps = { disabledLabel?: string; enabledLabel?: string; padding?: string; + dataTour?: string; }; export default function ToggleButton({ @@ -19,6 +20,7 @@ export default function ToggleButton({ disabledLabel = "Show ", enabledLabel = "Hide ", padding = "px-2 py-1", + dataTour, }: ToggleButtonProps) { return (
), ]; @@ -343,6 +395,7 @@ export type Props = { saveCallback: () => void; deleteScratch: () => Promise; setDecompilationTabEnabled: (enabled: boolean) => void; + tourTargetsEnabled?: boolean; }; export default function ScratchToolbar(props: Props) { @@ -354,7 +407,7 @@ export default function ScratchToolbar(props: Props) { return ( <>