fix: include lesson stepper styles in build, restrict scramble to locale changes only
- Lesson stepper CSS was not committed (only in working tree), causing vertical layout in production - ScrambleText now only animates when locale changes, not on any text prop change (e.g. on/off toggle)
This commit is contained in:
@@ -4,17 +4,20 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onBeforeUnmount } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
text: { type: String, required: true },
|
||||
});
|
||||
|
||||
const { locale } = useI18n();
|
||||
const displayText = ref(props.text);
|
||||
let frameId = null;
|
||||
let lastLocale = locale.value;
|
||||
|
||||
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZアイウエオカキクケコ漢字書道';
|
||||
const DURATION = 400; // total animation time in ms
|
||||
const STEPS_PER_CHAR = 3; // how many random chars before settling
|
||||
const DURATION = 400;
|
||||
const STEPS_PER_CHAR = 3;
|
||||
|
||||
function scrambleTo(newText) {
|
||||
if (frameId) cancelAnimationFrame(frameId);
|
||||
@@ -22,8 +25,6 @@ function scrambleTo(newText) {
|
||||
const oldText = displayText.value;
|
||||
const maxLen = Math.max(oldText.length, newText.length);
|
||||
const startTime = performance.now();
|
||||
|
||||
// Each character position settles at a staggered time
|
||||
const charDelay = DURATION / (maxLen || 1);
|
||||
|
||||
const tick = (now) => {
|
||||
@@ -35,15 +36,12 @@ function scrambleTo(newText) {
|
||||
const charElapsed = elapsed - charSettleTime;
|
||||
|
||||
if (i >= newText.length && elapsed > charSettleTime + charDelay) {
|
||||
// This position should disappear — don't add anything
|
||||
continue;
|
||||
}
|
||||
|
||||
if (charElapsed >= charDelay) {
|
||||
// This character has settled — show final
|
||||
if (i < newText.length) result += newText[i];
|
||||
} else if (charElapsed > 0) {
|
||||
// This character is still scrambling
|
||||
const step = Math.floor((charElapsed / charDelay) * STEPS_PER_CHAR);
|
||||
if (step >= STEPS_PER_CHAR - 1 && i < newText.length) {
|
||||
result += newText[i];
|
||||
@@ -51,7 +49,6 @@ function scrambleTo(newText) {
|
||||
result += CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||
}
|
||||
} else {
|
||||
// Hasn't started yet — show old char or space
|
||||
result += i < oldText.length ? oldText[i] : ' ';
|
||||
}
|
||||
}
|
||||
@@ -69,8 +66,21 @@ function scrambleTo(newText) {
|
||||
frameId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
// Only scramble when locale changes, otherwise update silently
|
||||
watch(() => props.text, (newVal) => {
|
||||
scrambleTo(newVal);
|
||||
if (locale.value !== lastLocale) {
|
||||
lastLocale = locale.value;
|
||||
scrambleTo(newVal);
|
||||
} else {
|
||||
displayText.value = newVal;
|
||||
}
|
||||
});
|
||||
|
||||
// Also watch locale directly to catch the change
|
||||
watch(locale, () => {
|
||||
// The text prop will update right after locale changes,
|
||||
// so we just mark it and let the text watcher handle animation
|
||||
lastLocale = '__pending__';
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -19,3 +19,151 @@
|
||||
.lesson-canvas-wrapper {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// ── Phase Stepper ──────────────────────────────────
|
||||
|
||||
.lesson-stepper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.lesson-stepper__step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lesson-stepper__dot {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: $radius-circle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $font-xs;
|
||||
font-weight: $weight-bold;
|
||||
background: $color-surface-light;
|
||||
color: $color-text-grey;
|
||||
border: $border-width-md solid transparent;
|
||||
transition: all $duration-normal $ease-default;
|
||||
|
||||
.is-active & {
|
||||
background: $color-primary;
|
||||
color: $color-text-dark;
|
||||
border-color: $color-primary;
|
||||
box-shadow: $shadow-glow-base;
|
||||
}
|
||||
|
||||
.is-done & {
|
||||
background: $color-success;
|
||||
color: $color-text-white;
|
||||
border-color: $color-success;
|
||||
}
|
||||
}
|
||||
|
||||
.lesson-stepper__label {
|
||||
font-size: 0.6rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: $weight-bold;
|
||||
color: $color-text-grey;
|
||||
opacity: $opacity-inactive;
|
||||
transition: opacity $duration-normal $ease-default;
|
||||
white-space: nowrap;
|
||||
|
||||
.is-active & {
|
||||
opacity: $opacity-active;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.is-done & {
|
||||
opacity: 0.6;
|
||||
color: $color-success;
|
||||
}
|
||||
}
|
||||
|
||||
.lesson-stepper__line {
|
||||
flex: 1;
|
||||
height: $border-width-md;
|
||||
background: $color-surface-light;
|
||||
border-radius: $radius-pill;
|
||||
transition: background $duration-normal $ease-default;
|
||||
margin: 0.875rem $spacing-xs 0;
|
||||
|
||||
&.is-done {
|
||||
background: $color-success;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Practice Dots ──────────────────────────────────
|
||||
|
||||
.practice-dots {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.practice-dot {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: $radius-circle;
|
||||
background: $color-surface-light;
|
||||
transition: all $duration-normal $ease-out-back;
|
||||
|
||||
&.is-filled {
|
||||
background: $color-primary;
|
||||
box-shadow: 0 0 0.5rem $color-primary;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mnemonic Box ───────────────────────────────────
|
||||
|
||||
.mnemonic-box {
|
||||
width: 100%;
|
||||
background: $bg-glass-subtle;
|
||||
border: $border-width-sm solid $color-border;
|
||||
}
|
||||
|
||||
// ── Lesson Summary ─────────────────────────────────
|
||||
|
||||
.lesson-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.lesson-summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: $spacing-md $spacing-sm;
|
||||
background: $bg-glass-subtle;
|
||||
border-radius: $radius-lg;
|
||||
border: $border-width-sm solid $color-border;
|
||||
transition: border-color $duration-normal $ease-default;
|
||||
|
||||
&:hover {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.lesson-summary-char {
|
||||
font-size: $font-xl;
|
||||
font-weight: $weight-bold;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.lesson-summary-meaning {
|
||||
font-size: $font-xs;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// ── Utility ────────────────────────────────────────
|
||||
|
||||
.stepper-spacer {
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user