Skip to content
Open
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
3 changes: 2 additions & 1 deletion css/components/_pretext.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
@use 'elements/poem';
@use 'elements/prism';
@use 'elements/math';
@use 'elements/misc-content';
@use 'elements/misc-content';
@use 'elements/veil';
111 changes: 111 additions & 0 deletions css/components/elements/_veil.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/* ==========================================================================
PreTeXt <veil> — final SCSS (light: royal blue, dark: burnt orange)
- Transparent background for revealed content
- Non-transparent button background (mode-aware)
- Works for inline (.veil--inline) and block (.veil--block) veils
- Includes keyboard focus and pressed feedback
========================================================================== */

/* ---------------- Theme tokens via CSS variables ------------------------- */
:root {
--veil-primary: #1d4ed8; /* royal blue */
--veil-btn-bg: #f8fafc; /* very light gray-blue */
--veil-text-color: inherit;

--veil-focus-ring: rgba(29, 78, 216, 0.35);
--veil-hover-ring: rgba(29, 78, 216, 0.18);
--veil-pressed-shadow: rgba(0,0,0,0.06);
}

/* Dark mode uses a burnt orange accent; PreTeXt toggles .dark-mode on <html> */
.dark-mode {
--veil-primary: #f59e0b; /* burnt orange */
--veil-btn-bg: rgba(245, 158, 11, 0.12);
--veil-text-color: inherit;

--veil-focus-ring: rgba(245, 158, 11, 0.45);
--veil-hover-ring: rgba(245, 158, 11, 0.22);
--veil-pressed-shadow: rgba(245, 158, 11, 0.16);
}

/* ---------------- Containers -------------------------------------------- */
.veil--inline { display: inline; }
.veil--block { margin: 0.25rem 0; }

/* ---------------- Reveal button (shown when hidden) --------------------- */
.veil .veil-toggle {
display: inline-block;
background: var(--veil-btn-bg);
color: var(--veil-primary);
padding: 0.06rem 0.35rem;
// border: 1px solid color-mix(in srgb, var(--veil-primary) 35%, transparent);
border: 1px solid currentColor;
border-radius: 0.25rem;
font-size: 0.85rem;
line-height: 1.2;
cursor: pointer;
vertical-align: baseline;
transition: box-shadow 120ms ease, transform 60ms ease, border-color 120ms ease;

&:hover {
box-shadow: 0 0 0 2px var(--veil-hover-ring);
}
&:active {
transform: scale(0.995);
}
&:focus-visible {
outline: 2px solid var(--veil-focus-ring);
outline-offset: 2px;
}
}

/* ---------------- Hidden by default ------------------------------------ */
.veil .veil-content {
display: none;
}

/* ---------------- Revealed state --------------------------------------- */
.veil.revealed .veil-toggle { display: none; }

.veil.revealed .veil-content {
display: inline; /* overridden to block for block variant */
background: transparent; /* transparent background as requested */
color: var(--veil-text-color);
border: 1px solid var(--veil-primary);
padding: 0.06rem 0.30rem;
border-radius: 0.20rem;
cursor: pointer;
transition: transform 60ms ease, box-shadow 120ms ease, border-color 120ms ease;

&:hover {
box-shadow: 0 0 0 2px var(--veil-hover-ring) inset;
}
&:focus-visible {
outline: 2px solid var(--veil-focus-ring);
outline-offset: 2px;
}

/* Pressed feedback (toggled by JS on pointer events) */
&.is-pressed {
transform: scale(0.995);
box-shadow: 0 0 0 3px var(--veil-pressed-shadow) inset;
}
}

/* Block variant shows content as a block when revealed */
.veil--block.revealed .veil-content {
display: block;
padding: 0.40rem 0.55rem;
}

/* ---------------- Print (HTML -> paper) --------------------------------- */
@media print {
.veil .veil-toggle { display: none !important; }
.veil .veil-content {
display: inline !important;
border: none !important;
background: transparent !important;
padding: 0 !important;
}
.veil--block .veil-content { display: block !important; }
}
68 changes: 68 additions & 0 deletions examples/sample-article/sample-article.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10677,6 +10677,74 @@ along with MathBook XML. If not, see <http://www.gnu.org/licenses/>.
</subsection>
</section>

<section xml:id="section-veil-testing">
<title><c>veil</c> Testing</title>

<p>
The function <veil><m>f(t) = e^{-t}</m></veil> satisfies the differential equation
<me>f'(t) = -f(t)</me>,
which is characteristic of <veil>exponential</veil> decay.
</p>

<p>
This sentence contains a <veil>simple hidden phrase</veil> and continues normally.
</p>

<p>
Here is a veil wrapping <veil><em>emphasized hidden text</em></veil> within an emphasized span.
</p>

<p>
You can also hide <veil>inline math like <m>f(t) = e^{-t}</m></veil> without breaking the flow.
</p>

<p>
A slightly more complex example with a link:
Learn more at <url href="https://pretextbook.org">PreTeXt</url> or explore <m>g(x) = x^2 + 1</m>.
<veil>
Learn more at PreTeXt.
</veil>
</p>

<p>
Testing long hidden text:
<veil>
This is a very long phrase that includes <em>emphasis</em>,
inline math <m>\int_0^1 x^2 dx = \frac{1}{3}</m>, and even a short sentence to see if line wrapping behaves properly.
</veil>
</p>

<p>
Testing long hidden text with p tag:
<veil>
<p>
This is a very long phrase that includes <em>emphasis</em>,
inline math <m>\int_0^1 x^2 dx = \frac{1}{3}</m>, and even a short sentence to see if line wrapping behaves properly.
</p>
</veil>
</p>

<p>
Multiple consecutive veils: <fillin> \cos(t) </fillin>
<veil>first hidden</veil>, then some regular text, and <veil>second hidden part</veil>.
</p>

<me>
h(t) = <fillin> \sin(t)</fillin> <veil>tiger</veil>
</me>

<veil>
<me>
h(t) = \int \sin(t)\ dt
</me>
</veil>

<p>
Final test: a veil at the <veil>end of the sentence</veil>.
</p>

</section>

<!--
Various author tools are planned. Note the embedded "todo"
elements above. An XSL transform in-testing will extract them
Expand Down
140 changes: 140 additions & 0 deletions js/pretext_add_on.js
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,146 @@ document.addEventListener("click", (ev) => {
setTimeout(() => button.classList.toggle("copied"), 1000);
});

/**
* <veil> interaction
* ------------------
* Purpose: Provide show/hide behavior for <veil> in HTML:
* - When hidden: show a small button (.veil-toggle)
* - When revealed: show bordered content (.veil-content) that can be clicked to hide
*
* Notes:
* - Real <button> for the toggle, with aria-expanded kept in sync
* - Focus moves into revealed content; hiding returns focus to the toggle
* - Enter/Space work on both the toggle and the revealed content
* - Clicks on interactive descendants (<a>, <input>, etc.) do not hide
* - MathJax: On first reveal, request typesetting of the revealed node
*/
(function () {
'use strict';

// Prevent double-initialization if this script runs more than once
if (window.__PTX_VEIL_INIT__) return;
window.__PTX_VEIL_INIT__ = true;

// Centralized selectors
const SEL = {
container: '.veil',
toggle: '.veil .veil-toggle',
revealedContent: '.veil.revealed .veil-content',
pressed: '.veil.revealed .veil-content.is-pressed',
};

// Elements considered "interactive" inside .veil-content: don't toggle on them
const INTERACTIVE = 'a, button, input, textarea, select, label, summary, details';

function isInteractive(target) {
return !!target.closest(INTERACTIVE);
}

function parts(container) {
return {
toggle: container.querySelector('.veil-toggle'),
content: container.querySelector('.veil-content'),
};
}

function typesetMath(node) {
// MathJax v3
if (window.MathJax && typeof window.MathJax.typesetPromise === 'function') {
window.MathJax.typesetPromise([node]).catch(() => {});
}
// Fallback: MathJax v2
else if (window.MathJax && window.MathJax.Hub && typeof window.MathJax.Hub.Queue === 'function') {
try { window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, node]); } catch (_) {}
}
}

function reveal(container) {
if (!container || container.classList.contains('revealed')) return;
const { toggle, content } = parts(container);
if (!content) return;

container.classList.add('revealed');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
content.removeAttribute('hidden');

// Move focus into content for keyboard users
content.focus({ preventScroll: true });

// Typeset any newly-visible math
typesetMath(content);
}

function hide(container) {
if (!container || !container.classList.contains('revealed')) return;
const { toggle, content } = parts(container);

container.classList.remove('revealed');
if (toggle) toggle.setAttribute('aria-expanded', 'false');
if (content) content.setAttribute('hidden', 'hidden');

// Return focus to the toggle for predictable keyboard flow
if (toggle) toggle.focus({ preventScroll: true });

// Clear pressed visual if present
if (content) content.classList.remove('is-pressed');
}

// Click: reveal via chip, hide by clicking inside revealed content (except on interactive children)
document.addEventListener('click', (evt) => {
const btn = evt.target.closest(SEL.toggle);
if (btn) {
reveal(btn.closest(SEL.container));
return;
}

const content = evt.target.closest(SEL.revealedContent);
if (content) {
if (isInteractive(evt.target)) return; // allow links/inputs to work normally
hide(content.closest(SEL.container));
}
});

// Keyboard: Enter/Space on either the chip or the revealed content
document.addEventListener('keydown', (evt) => {
const key = evt.key;
if (key !== 'Enter' && key !== ' ') return;

const btn = evt.target.closest(SEL.toggle);
if (btn) {
evt.preventDefault();
reveal(btn.closest(SEL.container));
return;
}

const content = evt.target.closest(SEL.revealedContent);
if (content) {
evt.preventDefault();
hide(content.closest(SEL.container));
}
});

// Pressed visual: add on pointerdown; remove on any pointerup/cancel anywhere
document.addEventListener('pointerdown', (evt) => {
const content = evt.target.closest(SEL.revealedContent);
if (!content || isInteractive(evt.target)) return;
content.classList.add('is-pressed');
});

function clearPressed() {
document.querySelectorAll(SEL.pressed).forEach((el) => el.classList.remove('is-pressed'));
}
document.addEventListener('pointerup', clearPressed);
document.addEventListener('pointercancel', clearPressed);

// Initialize ARIA explicitly (useful if HTML was serialized without it)
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll(SEL.toggle).forEach((btn) => {
if (!btn.hasAttribute('aria-expanded')) btn.setAttribute('aria-expanded', 'false');
});
});
})();

document.addEventListener("DOMContentLoaded", () => {
const elements = document.querySelectorAll(".clipboardable");
for (el of elements) {
Expand Down
5 changes: 5 additions & 0 deletions xsl/pretext-common.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,11 @@ Book (with parts), "section" at level 3
<xsl:message>PTX:BUG: current conversion needs an implementation of the "code-wrapper" template</xsl:message>
</xsl:template>

<xsl:template match="veil">
<xsl:message>PTX:WARNING: &lt;veil&gt; encountered but this conversion has no implementation; content suppressed.</xsl:message>
<!-- no output by default in -common -->
<xsl:text></xsl:text>
</xsl:template>

<!-- The content of a "pre" element is wrapped many ways, -->
<!-- but the content itself is always strictly text -->
Expand Down
Loading