Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/* === Smalruby: This file is Smalruby-specific (Ruby script preview panel) === */

@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/z-index.css";

/* Full-screen overlay (pointer-events: none so it doesn't block the editor) */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
pointer-events: none;
z-index: $z-index-card;
}

/* Draggable floating panel */
.panel-container {
background: white;
border-radius: 8px;
min-width: 600px;
max-width: 50vw;
display: flex;
flex-direction: column;
position: absolute;
overflow: hidden;
box-shadow: 0px 5px 25px 5px $ui-black-transparent;
pointer-events: auto;
}

/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: $motion-primary;
cursor: move;
user-select: none;
flex-shrink: 0;
}

.header-title {
display: flex;
gap: 8px;
align-items: center;
font-size: 0.875rem;
font-weight: 700;
color: white;
}

.header-buttons {
display: flex;
gap: 4px;
}

.header-button {
display: flex;
background: none;
border: none;
cursor: pointer;
width: 28px;
height: 28px;
padding: 4px;
border-radius: 4px;
align-items: center;
justify-content: center;
}

.header-button:hover {
background-color: $ui-black-transparent;
}

.header-button img {
width: 100%;
height: 100%;
}

/* Body content */
.body {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}

/* Code area */
.code-area {
flex: 1;
overflow: auto;
margin: 0;
padding: 12px 16px;
font-family: "Hack", "Source Code Pro", monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre;
background: #fafafa;
color: #333;
user-select: text;
cursor: text;
tab-size: 2;
}

/* Footer with copy button */
.footer {
display: flex;
justify-content: flex-end;
padding: 8px 12px;
border-top: 1px solid $ui-tertiary;
background: white;
flex-shrink: 0;
}

.copy-button {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border: 1px solid $ui-tertiary;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 0.8rem;
color: #575e75;
transition: background 0.15s;
}

.copy-button:hover {
background: #f0f0f0;
}

.copy-button-copied {
composes: copy-button;
background: $pen-primary;
color: white;
border-color: $pen-primary;
}

.copy-button-copied:hover {
background: $pen-primary;
}

/* highlight.js token colors (GitHub-light-like theme) */
.code-area :global(.hljs-keyword),
.code-area :global(.hljs-selector-tag),
.code-area :global(.hljs-built_in) {
color: #d73a49;
font-weight: bold;
}

.code-area :global(.hljs-string),
.code-area :global(.hljs-attr) {
color: #032f62;
}

.code-area :global(.hljs-comment),
.code-area :global(.hljs-quote) {
color: #6a737d;
font-style: italic;
}

.code-area :global(.hljs-number),
.code-area :global(.hljs-literal) {
color: #005cc5;
}

.code-area :global(.hljs-title),
.code-area :global(.hljs-class) {
color: #6f42c1;
}

.code-area :global(.hljs-variable),
.code-area :global(.hljs-name) {
color: #e36209;
}

.code-area :global(.hljs-symbol),
.code-area :global(.hljs-bullet) {
color: #0086b3;
}

.code-area :global(.hljs-params) {
color: #24292e;
}

.code-area :global(.hljs-subst) {
color: #24292e;
}

/* Hidden state for collapse */
.hidden {
display: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// === Smalruby: This file is Smalruby-specific (Ruby script preview panel) ===

import React, {useState, useCallback, useRef, useMemo} from 'react';
import PropTypes from 'prop-types';
import Draggable from 'react-draggable';
import {defineMessages, useIntl} from 'react-intl';
import hljs from 'highlight.js/lib/core';
import rubyLang from 'highlight.js/lib/languages/ruby';
import styles from './ruby-script-preview.css';

hljs.registerLanguage('ruby', rubyLang);

import closeIcon from '../cards/icon--close.svg';
import shrinkIcon from '../cards/icon--shrink.svg';
import expandIcon from '../cards/icon--expand.svg';

const MENU_BAR_HEIGHT = 48;

const messages = defineMessages({
title: {
id: 'gui.rubyScriptPreview.title',
defaultMessage: 'Preview Ruby Script',
description: 'Title for the Ruby script preview panel'
},
copy: {
id: 'gui.rubyScriptPreview.copy',
defaultMessage: 'Copy to Clipboard',
description: 'Button to copy Ruby script to clipboard'
},
copied: {
id: 'gui.rubyScriptPreview.copied',
defaultMessage: 'Copied!',
description: 'Shown after copying Ruby script to clipboard'
},
shrink: {
id: 'gui.rubyScriptPreview.shrink',
defaultMessage: 'Shrink',
description: 'Title for button to shrink the preview panel'
},
expand: {
id: 'gui.rubyScriptPreview.expand',
defaultMessage: 'Expand',
description: 'Title for button to expand the preview panel'
},
close: {
id: 'gui.rubyScriptPreview.close',
defaultMessage: 'Close',
description: 'Title for button to close the preview panel'
}
});

const RubyScriptPreview = ({code, onClose}) => {
const intl = useIntl();
const [expanded, setExpanded] = useState(true);
const [copied, setCopied] = useState(false);
const copiedTimerRef = useRef(null);

const handleShrinkExpand = useCallback(() => {
setExpanded(prev => !prev);
}, []);

const highlightedHtml = useMemo(
() => hljs.highlight(code, {language: 'ruby'}).value,
[code]
);

const handleCopy = useCallback(() => {
navigator.clipboard.writeText(code).then(() => {
setCopied(true);
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
copiedTimerRef.current = setTimeout(() => setCopied(false), 2000);
});
}, [code]);

// Position: right side, full height below menu bar
const panelWidth = 600;
const margin = 16;
const panelHeight = window.innerHeight - MENU_BAR_HEIGHT - (margin * 2);
const defaultPosition = {
x: window.innerWidth - panelWidth - margin,
y: MENU_BAR_HEIGHT + margin
};

return (
<div className={styles.overlay}>
<Draggable
bounds="parent"
defaultPosition={defaultPosition}
handle={`.${styles.header}`}
>
<div
className={styles.panelContainer}
style={{height: expanded ? `${panelHeight}px` : 'auto'}}
>
<div className={styles.header}>
<div className={styles.headerTitle}>
{intl.formatMessage(messages.title)}
</div>
<div className={styles.headerButtons}>
<button
className={styles.headerButton}
onClick={handleShrinkExpand}
title={intl.formatMessage(
expanded ? messages.shrink : messages.expand
)}
>
<img
draggable={false}
src={expanded ? shrinkIcon : expandIcon}
/>
</button>
<button
className={styles.headerButton}
onClick={onClose}
title={intl.formatMessage(messages.close)}
>
<img
draggable={false}
src={closeIcon}
/>
</button>
</div>
</div>
<div className={expanded ? styles.body : styles.hidden}>
{/* eslint-disable react/no-danger */}
<pre
className={styles.codeArea}
dangerouslySetInnerHTML={{__html: highlightedHtml}}
/>
{/* eslint-enable react/no-danger */}
<div className={styles.footer}>
<button
className={copied ? styles.copyButtonCopied : styles.copyButton}
onClick={handleCopy}
>
{intl.formatMessage(
copied ? messages.copied : messages.copy
)}
</button>
</div>
</div>
</div>
</Draggable>
</div>
);
};

RubyScriptPreview.propTypes = {
code: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired
};

export default RubyScriptPreview;
5 changes: 5 additions & 0 deletions packages/scratch-gui/src/components/ruby-toolbar/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ const messages = defineMessages({
defaultMessage: 'Insert class',
description: 'Label for insert class menu item in More menu'
},
previewRubyScript: {
id: 'gui.rubyToolbar.previewRubyScript',
defaultMessage: 'Preview Ruby script',
description: 'Label for preview Ruby script menu item in More menu'
},
stage: {
id: 'gui.rubyToolbar.stage',
defaultMessage: 'Stage',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ const RubyToolbar = props => {
if (props.onOpenAutoCorrectSettings) props.onOpenAutoCorrectSettings();
}, [props]);

const handlePreviewRubyScript = useCallback(() => {
setShowMoreMenu(false);
if (props.onDismissBubble) props.onDismissBubble();
if (props.onPreviewRubyScript) props.onPreviewRubyScript();
}, [props]);

const handleExecuteLine = useCallback(() => {
if (!props.editorRef) return;
const position = props.editorRef.getPosition();
Expand Down Expand Up @@ -259,6 +265,13 @@ const RubyToolbar = props => {
<span className={styles.moreMenuIcon}>{'{ }'}</span>
{intl.formatMessage(messages.insertClass)}
</div>
<div
className={styles.moreMenuItem}
onClick={handlePreviewRubyScript}
>
<span className={styles.moreMenuIcon}>{'</>'}</span>
{intl.formatMessage(messages.previewRubyScript)}
</div>
<div
className={styles.moreMenuItem}
onClick={handleOpenAutoCorrectSettings}
Expand Down Expand Up @@ -295,7 +308,8 @@ RubyToolbar.propTypes = {
onToggleFurigana: PropTypes.func,
autoCorrectEnabled: PropTypes.bool,
onToggleAutoCorrect: PropTypes.func,
onOpenAutoCorrectSettings: PropTypes.func
onOpenAutoCorrectSettings: PropTypes.func,
onPreviewRubyScript: PropTypes.func
};

export default RubyToolbar;
Loading
Loading