Quran-multi-aligner / src /ui /styles.py
hetchyy's picture
Upload folder using huggingface_hub
602b5d3 verified
"""CSS styles for the Quran Aligner Gradio interface."""
from config import (
ANIM_WORD_COLOR,
QURAN_TEXT_SIZE_PX, ARABIC_WORD_SPACING,
MEGA_TEXT_SIZE_DEFAULT, MEGA_LINE_SPACING_DEFAULT,
MEGA_WORD_SPACING_DEFAULT, MEGA_SURAH_LIGATURE_SIZE,
)
from data.font_data import DIGITAL_KHATT_FONT_B64, SURAH_NAME_FONT_B64
def build_css() -> str:
"""Return the complete CSS string for the Gradio interface."""
return f"""
/* Font faces */
@font-face {{
font-family: 'DigitalKhatt';
src: url(data:font/otf;base64,{DIGITAL_KHATT_FONT_B64}) format('opentype');
font-weight: normal;
font-style: normal;
}}
@font-face {{
font-family: 'SurahName';
src: url(data:font/truetype;base64,{SURAH_NAME_FONT_B64}) format('truetype');
font-weight: normal;
font-style: normal;
}}
.arabic-text {{
font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
direction: rtl;
text-align: right;
}}
/* Prevent output area from being in a scrolling box */
.gradio-container .prose {{
max-height: none !important;
}}
.output-html {{
max-height: none !important;
overflow: visible !important;
}}
/* Segment cards - theme adaptive */
.segment-card {{
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 12px;
border: 2px solid;
}}
.segment-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}}
.segment-title {{
font-size: 13px;
opacity: 0.9;
}}
.segment-badges {{
display: flex;
gap: 6px;
align-items: center;
}}
.segment-badge {{
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}}
.segment-audio {{
margin: 8px 0;
display: flex;
align-items: center;
gap: 8px;
}}
.segment-audio audio {{
flex: 1;
height: 32px;
border-radius: 4px;
}}
/* Lazy play button (replaces <audio controls> until clicked) */
.play-btn {{
flex: 1;
height: 32px;
border-radius: 4px;
border: 1px solid var(--border-color-primary, #ddd);
background: var(--block-background-fill, #f7f7f7);
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}}
.play-btn:hover {{ background: var(--block-background-fill-secondary, #eee); }}
/* Make color picker popup overlay instead of pushing content down */
.gradio-container .color-picker {{
position: relative;
overflow: visible !important;
}}
.gradio-container .color-picker .overflow-hidden,
.gradio-container .color-picker > div:last-child:not(:first-child) {{
position: absolute;
z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
border-radius: 8px;
overflow: visible !important;
max-height: none !important;
}}
.gradio-container .color-picker .overflow-hidden {{
overflow: visible !important;
}}
.gradio-container .color-picker *,
.gradio-container .color-picker div {{
overflow: visible !important;
max-height: none !important;
scrollbar-width: none !important;
}}
.gradio-container .color-picker *::-webkit-scrollbar {{
display: none !important;
}}
/* Ensure all color-picker ancestors allow overflow for absolute popup */
#anim-settings-accordion,
#anim-settings-accordion > *,
#anim-style-row,
#anim-style-row > * {{
overflow: visible !important;
}}
/* Animate button */
.animate-btn {{
background: #4a90d9 !important;
color: white !important;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
white-space: nowrap;
}}
.animate-btn:hover:not(:disabled) {{ background: #357abd !important; }}
.animate-btn.active {{ background: #dc3545 !important; }}
.animate-btn:disabled {{ background: #888 !important; cursor: not-allowed; opacity: 0.5; }}
.animate-btn.mfa-stale {{ background: #6c757d !important; }}
/* Make the HTML wrapper inside ts-row match the Gradio Button wrapper */
#ts-row > .gr-html {{
padding: 0;
margin: 0;
min-width: 0;
flex: 1 1 0%;
}}
#ts-row > div:has(> .animate-all-btn) {{
padding: 0;
margin: 0;
min-width: 0;
flex: 1 1 0%;
}}
/* Animate All button — matches Gradio lg button sizing */
.animate-all-btn {{
display: block;
width: 100%;
background: var(--button-primary-background-fill, #f97316) !important;
color: var(--button-primary-text-color, white) !important;
border: var(--button-primary-border, none);
padding: var(--size-2, 0.5rem) var(--size-4, 1rem);
border-radius: var(--button-large-radius, var(--radius-lg, 8px));
cursor: pointer;
font-size: var(--button-large-text-size, var(--text-lg, 1.125rem));
font-weight: var(--button-large-text-weight, 600);
box-sizing: border-box;
line-height: var(--line-md, 1.5);
min-height: var(--size-10, 40px);
}}
.animate-all-btn:hover:not(:disabled) {{ background: var(--button-primary-background-fill-hover, #ea6c10) !important; }}
.animate-all-btn.active {{ background: #dc3545 !important; }}
.animate-all-btn:disabled {{ background: #888 !important; cursor: not-allowed; opacity: 0.5; }}
/* Mega card for Animate All */
.mega-card {{
font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
font-size: {MEGA_TEXT_SIZE_DEFAULT}px;
direction: rtl;
text-align: justify;
line-height: {MEGA_LINE_SPACING_DEFAULT};
word-spacing: {MEGA_WORD_SPACING_DEFAULT}em;
padding: 16px;
border-radius: 8px;
background: var(--block-background-fill);
max-height: 70vh;
overflow-y: auto;
scrollbar-color: rgba(255,255,255,0.2) transparent;
}}
.mega-card::-webkit-scrollbar {{ width: 8px; }}
.mega-card::-webkit-scrollbar-track {{ background: transparent; }}
.mega-card::-webkit-scrollbar-thumb {{ background: rgba(255,255,255,0.2); border-radius: 4px; }}
.mega-card::-webkit-scrollbar-thumb:hover {{ background: rgba(255,255,255,0.35); }}
.mega-text-flow {{
display: inline;
}}
.mega-special-line {{
display: block;
text-align: center;
margin: 8px 0;
opacity: 0.7;
font-size: 0.85em;
}}
.mega-surah-separator {{
display: block;
text-align: center;
margin: 8px 0 2px;
padding: 4px 0 0;
border-top: 1px solid rgba(255,255,255,0.1);
opacity: 0.8;
font-family: 'SurahName', sans-serif;
font-feature-settings: "liga" 1;
font-size: {MEGA_SURAH_LIGATURE_SIZE}em;
line-height: 1.2;
letter-spacing: normal;
}}
.segment-card.hidden-for-mega {{ display: none; }}
.mega-top-bar {{
display: flex; justify-content: center; gap: 8px;
margin-top: 12px; margin-bottom: 8px;
}}
.mega-top-bar .animate-all-btn {{
width: auto; min-width: 0; min-height: auto;
padding: 4px 12px;
font-size: 12px; font-weight: bold;
border-radius: 4px;
line-height: normal;
box-sizing: border-box;
border: none;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
}}
.mega-exit-btn {{
background: #6c757d;
color: white;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
min-width: 0; min-height: auto;
line-height: normal;
box-sizing: border-box;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
}}
.mega-exit-btn:hover {{ background: #5a6268; }}
.mega-speed-select {{
/* Use Gradio's theme-aware variables */
background: var(--input-background-fill) !important;
color: var(--body-text-color) !important;
border: var(--input-border-width) solid var(--input-border-color) !important;
border-radius: var(--input-radius) !important;
/* Typography from Gradio */
font-size: var(--input-text-size) !important;
font-family: var(--font) !important;
/* Layout */
padding: var(--input-padding) !important;
height: 48px !important;
min-width: 70px;
box-sizing: border-box;
/* Dropdown styling */
cursor: pointer;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* Custom dropdown arrow using theme color */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='%236b7280' d='M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 6px center !important;
background-size: 16px !important;
padding-right: 28px !important;
}}
.mega-speed-select:hover {{
border-color: var(--input-border-color-hover) !important;
}}
.mega-speed-select:focus {{
border-color: var(--input-border-color-focus) !important;
outline: none;
box-shadow: var(--input-shadow-focus);
}}
.mega-speed-select option {{
background: var(--input-background-fill);
color: var(--body-text-color);
}}
.mega-tip {{
text-align: center; color: #b0b0b0; font-size: 0.95em;
padding: 8px 16px; margin-bottom: 6px;
background: rgba(255,255,255,0.05); border-radius: 8px;
border: 1px solid rgba(255,255,255,0.08);
width: fit-content; margin-left: auto; margin-right: auto;
}}
/* Word/char animation coloring — all modes use the window engine (JS-driven inline opacity) */
:root {{ --anim-word-color: {ANIM_WORD_COLOR}; }}
.word, .char {{
color: inherit;
}}
.word.active, .char.active, .word.active .char {{
color: var(--anim-word-color);
}}
/* Window engine: all hidden by default; JS sets inline opacity for visible window */
.anim-window .word {{ opacity: 0; }}
.anim-window .word.active {{ opacity: 1; }}
/* Character-level Window */
.anim-chars.anim-window .word {{ opacity: 1 !important; }}
.anim-chars.anim-window .char {{ opacity: 0; }}
.anim-chars.anim-window .char.active {{ opacity: 1; }}
/* Clickable words and verse markers in mega card */
.mega-text-flow .word {{ cursor: pointer; }}
.mega-text-flow .verse-marker {{ cursor: pointer; }}
/* Allow "All" hint below slider track to be visible */
#anim-window-prev, #anim-window-after {{ overflow: visible !important; padding-bottom: 1.2em; }}
#anim-window-prev *, #anim-window-after * {{ overflow: visible !important; }}
#anim-settings-accordion .block {{ border: none; }}
#anim-settings-accordion .color-picker {{ border: none !important; }}
#anim-settings-accordion .color-picker .block {{ border: none !important; }}
/* Merge style/granularity/color into one unified row */
#anim-style-row {{
gap: 0 !important;
border: 1px solid var(--border-color-primary, #ddd);
border-radius: var(--radius-lg, 8px);
overflow: visible;
}}
#anim-style-row > div {{
border: none !important;
box-shadow: none !important;
background: transparent !important;
}}
/* Side-by-side label + controls for animation settings */
#anim-style-row fieldset,
#anim-style-row > div:has(> .dialog-button) {{
display: flex !important;
flex-direction: row !important;
align-items: center !important;
gap: 8px;
}}
#anim-style-row .block-title,
#anim-style-row fieldset > span:first-child {{
white-space: nowrap;
min-width: fit-content;
margin: 0 !important;
font-size: 0.9em;
}}
.segment-text {{
font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
font-size: {QURAN_TEXT_SIZE_PX}px;
direction: rtl;
text-align: right;
line-height: 1.8;
word-spacing: {ARABIC_WORD_SPACING};
padding: 8px;
border-radius: 4px;
background: var(--block-background-fill);
}}
.segment-error {{
font-size: 12px;
margin-top: 4px;
color: var(--error-text-color, #dc3545);
}}
.no-match {{
opacity: 0.5;
}}
.no-segments {{
text-align: center;
opacity: 0.6;
padding: 40px;
}}
.segments-header {{
font-weight: bold;
margin-bottom: 16px;
}}
/* Confidence colors - light mode */
.segment-high {{ background: #d4edda; border-color: #28a745; }}
.segment-med {{ background: #fff3cd; border-color: #ffc107; }}
.segment-low {{ background: #f8d7da; border-color: #dc3545; }}
.segment-high-badge {{ background: #28a745; }}
.segment-med-badge {{ background: #ffc107; color: #333 !important; }}
.segment-low-badge {{ background: #dc3545; }}
.segment-special {{ background: #e8eaf6; border-color: #5c6bc0; border-style: dashed; }}
.segment-special-badge {{ background: #5c6bc0; }}
.segment-repeated-badge {{ background: #ffc107; color: #333 !important; }}
/* Repetition feedback widget */
.repeat-feedback-group {{
display: inline-flex;
align-items: center;
gap: 4px;
}}
.repeat-fb-btn {{
background: none;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 8px;
cursor: pointer;
font-size: 10px;
padding: 0 5px;
height: 20px;
min-height: 0;
min-width: 0;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.15s, transform 0.15s;
position: relative;
top: 2px;
}}
.repeat-fb-btn:hover {{
opacity: 1;
transform: scale(1.2);
}}
.repeat-fb-thanks {{
font-size: 10px;
color: #28a745;
font-weight: 600;
white-space: nowrap;
}}
.repeat-fb-form {{
display: flex;
gap: 4px;
align-items: center;
}}
.repeat-fb-textarea {{
font-size: 10px;
font-family: inherit;
padding: 0 5px;
border: 1px solid var(--border-color-primary, #ccc);
border-radius: 8px;
background: var(--input-background-fill, white);
color: var(--body-text-color, #333);
resize: none;
height: 20px;
min-height: 0;
box-sizing: border-box;
width: 156px;
overflow: hidden;
}}
.repeat-fb-submit {{
font-size: 10px;
padding: 0 6px;
border: none;
border-radius: 8px;
background: #28a745;
color: white;
cursor: pointer;
font-weight: 600;
white-space: nowrap;
height: 20px;
min-height: 0;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
}}
.repeat-fb-submit:hover {{ background: #218838; }}
/* Divider between repeated text lines */
.repeat-divider {{
height: 0;
border: none;
border-top: 1px dashed var(--border-color-primary, rgba(128,128,128,0.5));
margin: 2px 0;
}}
/* Missing words group — wraps two consecutive segments sharing a gap */
.missing-words-group {{
border: 2px dashed #dc3545;
border-radius: 10px;
padding: 8px;
margin-bottom: 12px;
}}
.missing-words-group .segment-card:last-child {{ margin-bottom: 0; }}
.missing-words-group-tag {{
background: #dc3545;
color: white;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
width: fit-content;
margin: 0 auto 8px;
}}
/* Hidden JS→Python bridge (must be in DOM but invisible) */
#ref-edit-bridge, #repeat-fb-bridge, #edit-patch {{
position: absolute !important;
left: -9999px !important;
height: 0 !important;
overflow: hidden !important;
pointer-events: none;
}}
#ref-edit-bridge *, #repeat-fb-bridge *, #edit-patch * {{ pointer-events: auto; }}
/* Clickable ref for inline editing */
.ref-editable {{
cursor: pointer;
border-bottom: 1px dashed currentColor;
}}
.ref-editable:hover {{
opacity: 0.7;
}}
.ref-edit-input {{
font-size: inherit;
font-family: inherit;
background: var(--input-background-fill, white);
color: var(--body-text-color, #333);
border: 1px solid var(--border-color-primary, #ddd);
border-radius: 4px;
padding: 0 4px;
width: 16ch;
}}
/* Review summary text colors */
.segments-review-summary {{ margin-bottom: 8px; font-size: 14px; }}
.segment-med-text {{ color: #856404; }}
.segment-low-text {{ color: #721c24; }}
@media (prefers-color-scheme: dark) {{
.segment-med-text {{ color: #ffc107; }}
.segment-low-text {{ color: #f8d7da; }}
}}
.dark .segment-med-text {{ color: #ffc107; }}
.dark .segment-low-text {{ color: #f8d7da; }}
/* Confidence colors - dark mode */
@media (prefers-color-scheme: dark) {{
.segment-high {{ background: rgba(40, 167, 69, 0.2); border-color: #28a745; }}
.segment-med {{ background: rgba(255, 193, 7, 0.2); border-color: #ffc107; }}
.segment-low {{ background: rgba(220, 53, 69, 0.2); border-color: #dc3545; }}
.segment-special {{ background: rgba(92, 107, 192, 0.2); border-color: #5c6bc0; border-style: dashed; }}
.missing-words-group {{ background: rgba(220, 53, 69, 0.05); }}
}}
/* Also support Gradio's dark class */
.dark .segment-high {{ background: rgba(40, 167, 69, 0.2); border-color: #28a745; }}
.dark .segment-med {{ background: rgba(255, 193, 7, 0.2); border-color: #ffc107; }}
.dark .segment-low {{ background: rgba(220, 53, 69, 0.2); border-color: #dc3545; }}
.dark .segment-special {{ background: rgba(92, 107, 192, 0.2); border-color: #5c6bc0; border-style: dashed; }}
.dark .missing-words-group {{ background: rgba(220, 53, 69, 0.05); }}
/* Input mode toggle */
#input-mode-row {{ gap: 0 !important; }}
#input-mode-row button {{
border-radius: 0 !important;
border: 1px solid var(--border-color-primary) !important;
}}
#input-mode-row button:first-child {{ border-radius: 8px 0 0 8px !important; }}
#input-mode-row button:last-child {{ border-radius: 0 8px 8px 0 !important; }}
#input-mode-row button:not(:first-child) {{ border-left: none !important; }}
#upload-panel, #record-panel, #link-panel,
#upload-panel > div, #record-panel > div, #link-panel > div {{ overflow: visible !important; }}
.mode-active {{
background: var(--button-primary-background-fill) !important;
color: var(--button-primary-text-color) !important;
border-color: var(--button-primary-background-fill) !important;
}}
/* Example / site pill buttons — joined group */
#example-row, #link-example-row {{ gap: 0 !important; }}
#example-row button, #link-example-row button {{
border-radius: 0 !important;
border: 1px solid var(--border-color-primary) !important;
}}
#example-row button:first-child, #link-example-row button:first-child {{ border-radius: 8px 0 0 8px !important; }}
#example-row button:last-child, #link-example-row button:last-child {{ border-radius: 0 8px 8px 0 !important; }}
#example-row button:not(:first-child), #link-example-row button:not(:first-child) {{ border-left: none !important; }}
"""