@expressive-code/plugin-frames
Advanced tools
Comparing version 0.4.1 to 0.5.0
import { StyleSettings, ExpressiveCodePlugin, AttachedPluginData } from '@expressive-code/core'; | ||
declare const framesStyleSettings: StyleSettings<"shadowColor" | "frameBoxShadowCssValue" | "editorActiveTabBackground" | "editorActiveTabForeground" | "editorActiveTabBorder" | "editorActiveTabBorderTop" | "editorActiveTabBorderBottom" | "editorActiveTabMarginInlineStart" | "editorActiveTabMarginBlockStart" | "editorTabBorderRadius" | "editorTabBarBackground" | "editorTabBarBorderColor" | "editorTabBarBorderBottom" | "editorBackground" | "terminalTitlebarDotsForeground" | "terminalTitlebarBackground" | "terminalTitlebarForeground" | "terminalTitlebarBorderBottom" | "terminalBackground">; | ||
declare const framesStyleSettings: StyleSettings<"shadowColor" | "frameBoxShadowCssValue" | "editorActiveTabBackground" | "editorActiveTabForeground" | "editorActiveTabBorder" | "editorActiveTabBorderTop" | "editorActiveTabBorderBottom" | "editorActiveTabMarginInlineStart" | "editorActiveTabMarginBlockStart" | "editorTabBorderRadius" | "editorTabBarBackground" | "editorTabBarBorderColor" | "editorTabBarBorderBottom" | "editorBackground" | "terminalTitlebarDotsForeground" | "terminalTitlebarBackground" | "terminalTitlebarForeground" | "terminalTitlebarBorderBottom" | "terminalBackground" | "inlineButtonForeground" | "inlineButtonHoverBackground" | "inlineButtonActiveBorder" | "tooltipSuccessBackground" | "tooltipSuccessForeground">; | ||
@@ -12,2 +12,7 @@ interface PluginFramesOptions { | ||
extractFileNameFromCode?: boolean; | ||
/** | ||
* If this is true (default), a "Copy to clipboard" button | ||
* will be shown for each code block. | ||
*/ | ||
showCopyToClipboardButton?: boolean; | ||
styleOverrides?: Partial<typeof framesStyleSettings.defaultSettings>; | ||
@@ -14,0 +19,0 @@ } |
@@ -6,3 +6,3 @@ // src/index.ts | ||
// src/styles.ts | ||
import { StyleSettings, multiplyAlpha, onBackground, toRgbaString } from "@expressive-code/core"; | ||
import { StyleSettings, multiplyAlpha, onBackground, toRgbaString, setAlpha } from "@expressive-code/core"; | ||
var framesStyleSettings = new StyleSettings({ | ||
@@ -27,3 +27,8 @@ shadowColor: ({ theme, coreStyles }) => theme.colors["widget.shadow"] || multiplyAlpha(coreStyles.borderColor, 0.75), | ||
terminalTitlebarBorderBottom: ({ theme, coreStyles }) => onBackground(coreStyles.borderColor, theme.type === "dark" ? "#000000bf" : "#ffffffbf"), | ||
terminalBackground: ({ theme }) => theme.colors["terminal.background"] | ||
terminalBackground: ({ theme }) => theme.colors["terminal.background"], | ||
inlineButtonForeground: ({ coreStyles }) => coreStyles.codeForeground, | ||
inlineButtonHoverBackground: ({ resolveSetting }) => setAlpha(resolveSetting("inlineButtonForeground"), 0.2), | ||
inlineButtonActiveBorder: ({ resolveSetting }) => setAlpha(resolveSetting("inlineButtonForeground"), 0.4), | ||
tooltipSuccessBackground: "#177d07", | ||
tooltipSuccessForeground: "white" | ||
}); | ||
@@ -46,2 +51,11 @@ function getFramesBaseStyles(theme, coreStyles, styleOverrides) { | ||
const terminalTitlebarDots = `url("data:image/svg+xml,${escapedDotsSvg}")`; | ||
const inlineButtonFg = toRgbaString(framesStyles.inlineButtonForeground); | ||
const copySvg = [ | ||
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='${inlineButtonFg}' stroke-width='1.75'>`, | ||
`<path d='M3 19a2 2 0 0 1-1-2V2a2 2 0 0 1 1-1h13a2 2 0 0 1 2 1'/>`, | ||
`<rect x='6' y='5' width='16' height='18' rx='1.5' ry='1.5'/>`, | ||
`</svg>` | ||
].join(""); | ||
const escapedCopySvg = copySvg.replace(/</g, "%3C").replace(/>/g, "%3E"); | ||
const copyToClipboard = `url("data:image/svg+xml,${escapedCopySvg}")`; | ||
const activeTabBackgrounds = []; | ||
@@ -63,4 +77,6 @@ if (framesStyles.editorActiveTabBorderTop) { | ||
all: unset; | ||
position: relative; | ||
display: block; | ||
--header-border-radius: calc(${coreStyles.borderRadius} + ${coreStyles.borderWidth}); | ||
--button-spacing: calc(${coreStyles.borderWidth} + 0.1rem); | ||
border-radius: var(--header-border-radius); | ||
@@ -126,2 +142,4 @@ box-shadow: ${framesStyles.frameBoxShadowCssValue}; | ||
&.is-terminal { | ||
--button-spacing: calc(${coreStyles.borderWidth} + 0.02rem); | ||
/* Terminal title bar */ | ||
@@ -167,2 +185,79 @@ & .header { | ||
} | ||
.copy { | ||
display: flex; | ||
gap: 0.25rem; | ||
flex-direction: row-reverse; | ||
position: absolute; | ||
top: var(--button-spacing); | ||
inset-inline-end: 0.5rem; | ||
button { | ||
align-self: flex-end; | ||
width: 1.75rem; | ||
height: 1.75rem; | ||
margin: 0; | ||
padding: 0.4rem; | ||
border-radius: 0.2rem; | ||
z-index: 2; | ||
cursor: pointer; | ||
background: transparent; | ||
border: ${coreStyles.borderWidth} solid transparent; | ||
opacity: 0.5; | ||
transition-property: opacity, background, border-color; | ||
transition-duration: 0.2s; | ||
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94); | ||
&:hover, &:focus:focus-visible { | ||
opacity: 0.75; | ||
background: ${framesStyles.inlineButtonHoverBackground}; | ||
} | ||
&:active { | ||
transition-duration: 0s; | ||
opacity: 1; | ||
border-color: ${framesStyles.inlineButtonActiveBorder}; | ||
} | ||
&::before { | ||
content: ${copyToClipboard}; | ||
line-height: 0; | ||
} | ||
} | ||
.feedback { | ||
--tooltip-arrow-size: 0.35rem; | ||
--tooltip-bg: ${framesStyles.tooltipSuccessBackground}; | ||
color: ${framesStyles.tooltipSuccessForeground}; | ||
pointer-events: none; | ||
user-select: none; | ||
-webkit-user-select: none; | ||
position: relative; | ||
align-self: center; | ||
background-color: var(--tooltip-bg); | ||
z-index: 99; | ||
padding: 0.125rem 0.75rem; | ||
border-radius: 0.2rem; | ||
margin-inline-end: var(--tooltip-arrow-size); | ||
opacity: 0; | ||
transition-property: opacity, transform; | ||
transition-duration: 0.2s; | ||
transition-timing-function: ease-in-out; | ||
transform: translate3d(0, 0.25rem, 0); | ||
&::after { | ||
position: absolute; | ||
content: ''; | ||
top: calc(50% - var(--tooltip-arrow-size)); | ||
inset-inline-end: calc(-2 * (var(--tooltip-arrow-size) - 0.5px)); | ||
border: var(--tooltip-arrow-size) solid transparent; | ||
border-inline-start-color: var(--tooltip-bg); | ||
} | ||
} | ||
button:focus + .feedback.show { | ||
opacity: 1; | ||
transform: translate3d(0, 0, 0); | ||
} | ||
} | ||
`; | ||
@@ -223,8 +318,91 @@ return styles; | ||
// src/copy-js-module.ts | ||
var domCopy = [ | ||
// Start of function | ||
`function domCopy(text) {`, | ||
// Create a new DOM element to copy from and append it to the document, | ||
// but make sure it's not visible and does not cause reflow | ||
`let n = document.createElement('pre'); | ||
Object.assign(n.style, { | ||
opacity: '0', | ||
pointerEvents: 'none', | ||
position: 'absolute', | ||
overflow: 'hidden', | ||
left: '0', | ||
top: '0', | ||
width: '20px', | ||
height: '20px', | ||
webkitUserSelect: 'auto', | ||
userSelect: 'all' | ||
}); | ||
n.ariaHidden = 'true'; | ||
n.textContent = text; | ||
document.body.appendChild(n);`, | ||
// Select the DOM element's contents | ||
`let r = document.createRange(); | ||
r.selectNode(n); | ||
let s = getSelection(); | ||
s.removeAllRanges(); | ||
s.addRange(r);`, | ||
// Copy the selection to the clipboard | ||
`let ok = false; | ||
try { | ||
ok = document.execCommand('copy'); | ||
} finally { | ||
s.removeAllRanges(); | ||
document.body.removeChild(n); | ||
} | ||
return ok;`, | ||
// End of function | ||
`}` | ||
]; | ||
var handleClicks = [ | ||
// Start of loop through all buttons that adds a click handler per button | ||
`document.querySelectorAll('[SELECTOR]').forEach((button) => | ||
button.addEventListener('click', async () => {`, | ||
// Click handler code | ||
`let ok = false; | ||
let code = button.dataset.code.replace(/\\u007f/g, '\\n'); | ||
try { | ||
await navigator.clipboard.writeText(code); | ||
ok = true; | ||
} catch (err) { | ||
ok = domCopy(code); | ||
}`, | ||
// Show feedback tooltip | ||
`if (ok && (!button.nextSibling || !button.nextSibling.classList.contains('feedback'))) { | ||
let tt = document.createElement('div'); | ||
tt.classList.add('feedback'); | ||
tt.append(button.dataset.copied); | ||
button.after(tt);`, | ||
// Use offsetWidth and requestAnimationFrame to opt out of DOM batching, | ||
// which helps to ensure that the transition on 'show' works | ||
` tt.offsetWidth; | ||
requestAnimationFrame(() => tt.classList.add('show'));`, | ||
// Hide & remove the tooltip again when we no longer need it | ||
` let h = () => { | ||
if (!(parseFloat(getComputedStyle(tt).opacity) > 0)) tt.remove(); | ||
}; | ||
setTimeout(() => tt.classList.remove('show'), 1500); | ||
setTimeout(() => h(), 2500); | ||
tt.addEventListener('transitioncancel', h); | ||
tt.addEventListener('transitionend', h); | ||
}`, | ||
// End of loop through all buttons | ||
`}))` | ||
]; | ||
var getCopyJsModule = (buttonSelector) => { | ||
return [...domCopy, ...handleClicks].map( | ||
(line) => line.trim().replace(/\s*[\r\n]\s*/g, "").replace(/\s*([:;,={}()<>])\s*/g, "$1").replace(/;}/g, "}") | ||
).join("").replace("[SELECTOR]", buttonSelector); | ||
}; | ||
// src/index.ts | ||
function pluginFrames(options = {}) { | ||
const extractFileNameFromCode = options.extractFileNameFromCode ?? true; | ||
const showCopyToClipboardButton = options.showCopyToClipboardButton ?? true; | ||
return { | ||
name: "Frames", | ||
baseStyles: ({ theme, coreStyles }) => getFramesBaseStyles(theme, coreStyles, options.styleOverrides || {}), | ||
jsModules: showCopyToClipboardButton ? [getCopyJsModule(`.expressive-code .copy button`)] : void 0, | ||
hooks: { | ||
@@ -264,2 +442,17 @@ preprocessMetadata: ({ codeBlock }) => { | ||
const screenReaderTitle = !titleText && isTerminal ? [h("span", { className: "sr-only" }, fallbackTerminalWindowTitle)] : []; | ||
const extraElements = []; | ||
if (showCopyToClipboardButton) { | ||
const copyButtonTooltip = "Copy to clipboard"; | ||
const copyButtonCopied = "Copied!"; | ||
const codeToCopy = codeBlock.code.replace(/\n/g, "\x7F"); | ||
extraElements.push( | ||
h("div", { className: "copy" }, [ | ||
h("button", { | ||
title: copyButtonTooltip, | ||
"data-copied": copyButtonCopied, | ||
"data-code": codeToCopy | ||
}) | ||
]) | ||
); | ||
} | ||
renderData.blockAst = h( | ||
@@ -278,2 +471,3 @@ "figure", | ||
h("figcaption", { className: "header" }, [...visibleTitle, ...screenReaderTitle]), | ||
...extraElements, | ||
// Render the original code block | ||
@@ -280,0 +474,0 @@ renderData.blockAst |
{ | ||
"name": "@expressive-code/plugin-frames", | ||
"version": "0.4.1", | ||
"version": "0.5.0", | ||
"description": "Frames plugin for Expressive Code. Wraps code blocks in a styled editor or terminal frame with support for titles, multiple tabs and more.", | ||
@@ -21,7 +21,7 @@ "keywords": [], | ||
"dependencies": { | ||
"@expressive-code/core": "^0.4.0", | ||
"@expressive-code/core": "^0.5.0", | ||
"hastscript": "^7.2.0" | ||
}, | ||
"devDependencies": { | ||
"@internal/test-utils": "^0.1.3", | ||
"@internal/test-utils": "^0.2.0", | ||
"hast-util-select": "^5.0.5", | ||
@@ -28,0 +28,0 @@ "hast-util-to-html": "^8.0.4" |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
125316
1433
+ Added@expressive-code/core@0.5.0(transitive)
- Removed@expressive-code/core@0.4.0(transitive)
Updated@expressive-code/core@^0.5.0