Wave 5: refactor generators batch 3 to crypto random
- refactor(generators): migrate Meal, Names, Number, Palette to crypto random - refactor(generators): migrate Password, Rps, Shuffler to crypto random
This commit is contained in:
+54
-2
@@ -8,7 +8,7 @@
|
||||
"plan_name": "critical-refactor",
|
||||
"status": "active",
|
||||
"started_at": "2026-05-13T16:25:19.266Z",
|
||||
"updated_at": "2026-05-13T17:25:32.173Z",
|
||||
"updated_at": "2026-05-13T17:34:08.166Z",
|
||||
"session_ids": [
|
||||
"ses_1de6ede59ffexUwt5C0sddEWwR"
|
||||
],
|
||||
@@ -94,6 +94,32 @@
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T17:25:32.173Z",
|
||||
"elapsed_ms": 20904
|
||||
},
|
||||
"todo:20": {
|
||||
"task_key": "todo:20",
|
||||
"task_label": "20",
|
||||
"task_title": "Refactor MealGenerator.astro",
|
||||
"session_id": "ses_1dda01e38ffeVc6AUwAlt7lQXl",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-05-13T17:31:02.031Z",
|
||||
"started_at": "2026-05-13T17:30:26.005Z",
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T17:31:02.031Z",
|
||||
"elapsed_ms": 36026
|
||||
},
|
||||
"todo:24": {
|
||||
"task_key": "todo:24",
|
||||
"task_label": "24",
|
||||
"task_title": "Refactor PasswordGenerator.astro",
|
||||
"session_id": "ses_1dd9b7bb1ffez3Q0aayg7PZUIF",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-05-13T17:34:08.166Z",
|
||||
"started_at": "2026-05-13T17:32:34.652Z",
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T17:34:08.166Z",
|
||||
"elapsed_ms": 93514
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +127,7 @@
|
||||
"active_plan": "/home/emil/Desktop/Coding/AI/Randify.pro/.sisyphus/plans/critical-refactor.md",
|
||||
"started_at": "2026-05-13T16:25:19.266Z",
|
||||
"status": "active",
|
||||
"updated_at": "2026-05-13T17:25:32.173Z",
|
||||
"updated_at": "2026-05-13T17:34:08.166Z",
|
||||
"session_ids": [
|
||||
"ses_1de6ede59ffexUwt5C0sddEWwR"
|
||||
],
|
||||
@@ -187,6 +213,32 @@
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T17:25:32.173Z",
|
||||
"elapsed_ms": 20904
|
||||
},
|
||||
"todo:20": {
|
||||
"task_key": "todo:20",
|
||||
"task_label": "20",
|
||||
"task_title": "Refactor MealGenerator.astro",
|
||||
"session_id": "ses_1dda01e38ffeVc6AUwAlt7lQXl",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-05-13T17:31:02.031Z",
|
||||
"started_at": "2026-05-13T17:30:26.005Z",
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T17:31:02.031Z",
|
||||
"elapsed_ms": 36026
|
||||
},
|
||||
"todo:24": {
|
||||
"task_key": "todo:24",
|
||||
"task_label": "24",
|
||||
"task_title": "Refactor PasswordGenerator.astro",
|
||||
"session_id": "ses_1dd9b7bb1ffez3Q0aayg7PZUIF",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-05-13T17:34:08.166Z",
|
||||
"started_at": "2026-05-13T17:32:34.652Z",
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T17:34:08.166Z",
|
||||
"elapsed_ms": 93514
|
||||
}
|
||||
},
|
||||
"agent": "atlas"
|
||||
|
||||
@@ -577,7 +577,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
- Message: `refactor(generators): migrate batch 2 to crypto random (Gradient, Hash, Letter, List, Lorem, Lottery, Magic8Ball)`
|
||||
- Files: `src/components/generators/{Gradient,Hash,Letter,List,Lorem,Lottery,Magic8Ball}Generator.astro`
|
||||
|
||||
- [ ] 20. Refactor MealGenerator.astro
|
||||
- [x] 20. Refactor MealGenerator.astro
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
**Parallelization**: YES (with Tasks 13-19, 21-26, 27-32)
|
||||
@@ -585,7 +585,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 21. Refactor NamesGenerator.astro
|
||||
- [x] 21. Refactor NamesGenerator.astro
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
**Parallelization**: YES (with Tasks 13-19, 20, 22-26, 27-32)
|
||||
@@ -593,7 +593,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 22. Refactor NumberGenerator.astro
|
||||
- [x] 22. Refactor NumberGenerator.astro
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
**Parallelization**: YES (with Tasks 13-19, 20-21, 23-26, 27-32)
|
||||
@@ -601,7 +601,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 23. Refactor PaletteGenerator.astro
|
||||
- [x] 23. Refactor PaletteGenerator.astro
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
**Parallelization**: YES (with Tasks 13-19, 20-22, 24-26, 27-32)
|
||||
@@ -609,7 +609,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 24. Refactor PasswordGenerator.astro
|
||||
- [x] 24. Refactor PasswordGenerator.astro
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
**Parallelization**: YES (with Tasks 13-19, 20-23, 25-26, 27-32)
|
||||
@@ -617,7 +617,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 25. Refactor RpsGenerator.astro
|
||||
- [x] 25. Refactor RpsGenerator.astro
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
**Parallelization**: YES (with Tasks 13-19, 20-24, 26, 27-32)
|
||||
@@ -625,7 +625,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 26. Refactor ShufflerGenerator.astro
|
||||
- [x] 26. Refactor ShufflerGenerator.astro
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
**Parallelization**: YES (with Tasks 13-19, 20-25, 27-32)
|
||||
|
||||
@@ -77,38 +77,7 @@ const timeFilters = isRu
|
||||
{ value: "slow", label: "Slow cook" },
|
||||
];
|
||||
|
||||
const cuisineLabelMap: Record<string, { en: string; ru: string }> = {
|
||||
italian: { en: "Italian", ru: "Итальянская" },
|
||||
asian: { en: "Asian", ru: "Азиатская" },
|
||||
mexican: { en: "Mexican", ru: "Мексиканская" },
|
||||
american: { en: "American", ru: "Американская" },
|
||||
mediterranean: { en: "Mediterranean", ru: "Средиземноморская" },
|
||||
french: { en: "French", ru: "Французская" },
|
||||
indian: { en: "Indian", ru: "Индийская" },
|
||||
russian: { en: "Russian", ru: "Русская" },
|
||||
"middle-eastern": { en: "Middle Eastern", ru: "Ближневосточная" },
|
||||
};
|
||||
|
||||
const mealTypeLabelMap: Record<string, { en: string; ru: string }> = {
|
||||
breakfast: { en: "Breakfast", ru: "Завтрак" },
|
||||
lunch: { en: "Lunch", ru: "Обед" },
|
||||
dinner: { en: "Dinner", ru: "Ужин" },
|
||||
snack: { en: "Snack", ru: "Перекус" },
|
||||
dessert: { en: "Dessert", ru: "Десерт" },
|
||||
};
|
||||
|
||||
const difficultyLabelMap: Record<string, { en: string; ru: string; color: string }> = {
|
||||
easy: { en: "Easy", ru: "Легко", color: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25" },
|
||||
medium: { en: "Medium", ru: "Средне", color: "bg-amber-500/15 text-amber-400 border-amber-500/25" },
|
||||
hard: { en: "Hard", ru: "Сложно", color: "bg-red-500/15 text-red-400 border-red-500/25" },
|
||||
};
|
||||
|
||||
const dietaryLabelMap: Record<string, { en: string; ru: string; emoji: string }> = {
|
||||
vegetarian: { en: "Vegetarian", ru: "Вегетарианское", emoji: "🌿" },
|
||||
vegan: { en: "Vegan", ru: "Веганское", emoji: "🌱" },
|
||||
"gluten-free": { en: "Gluten-Free", ru: "Без глютена", emoji: "🌾" },
|
||||
spicy: { en: "Spicy", ru: "Острое", emoji: "🌶" },
|
||||
};
|
||||
---
|
||||
|
||||
<div id="meal-generator" class="mt-8">
|
||||
@@ -273,9 +242,44 @@ const dietaryLabelMap: Record<string, { en: string; ru: string; emoji: string }>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script define:vars={{ cuisineLabelMap, mealTypeLabelMap, difficultyLabelMap, dietaryLabelMap }}>
|
||||
(function() {
|
||||
// Meals data
|
||||
<script>
|
||||
import { randomInt } from "@/lib/client/random";
|
||||
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const cuisineLabelMap = {
|
||||
italian: { en: "Italian", ru: "Итальянская" },
|
||||
asian: { en: "Asian", ru: "Азиатская" },
|
||||
mexican: { en: "Mexican", ru: "Мексиканская" },
|
||||
american: { en: "American", ru: "Американская" },
|
||||
mediterranean: { en: "Mediterranean", ru: "Средиземноморская" },
|
||||
french: { en: "French", ru: "Французская" },
|
||||
indian: { en: "Indian", ru: "Индийская" },
|
||||
russian: { en: "Russian", ru: "Русская" },
|
||||
"middle-eastern": { en: "Middle Eastern", ru: "Ближневосточная" },
|
||||
};
|
||||
|
||||
const mealTypeLabelMap = {
|
||||
breakfast: { en: "Breakfast", ru: "Завтрак" },
|
||||
lunch: { en: "Lunch", ru: "Обед" },
|
||||
dinner: { en: "Dinner", ru: "Ужин" },
|
||||
snack: { en: "Snack", ru: "Перекус" },
|
||||
dessert: { en: "Dessert", ru: "Десерт" },
|
||||
};
|
||||
|
||||
const difficultyLabelMap = {
|
||||
easy: { en: "Easy", ru: "Легко", color: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25" },
|
||||
medium: { en: "Medium", ru: "Средне", color: "bg-amber-500/15 text-amber-400 border-amber-500/25" },
|
||||
hard: { en: "Hard", ru: "Сложно", color: "bg-red-500/15 text-red-400 border-red-500/25" },
|
||||
};
|
||||
|
||||
const dietaryLabelMap = {
|
||||
vegetarian: { en: "Vegetarian", ru: "Вегетарианское", emoji: "🌿" },
|
||||
vegan: { en: "Vegan", ru: "Веганское", emoji: "🌱" },
|
||||
"gluten-free": { en: "Gluten-Free", ru: "Без глютена", emoji: "🌾" },
|
||||
spicy: { en: "Spicy", ru: "Острое", emoji: "🌶" },
|
||||
};
|
||||
|
||||
// Meals data
|
||||
const meals = [
|
||||
{ en: "Shakshuka", ru: "Шакшука", cuisine: "middle-eastern", type: "breakfast", difficulty: "easy", time: 25, desc: "Eggs poached in a simmering tomato sauce with spices.", ruDesc: "Яйца, приготовленные в томатном соусе со специями.", ingredients: ["eggs", "tomatoes", "bell pepper", "onion", "cumin", "paprika"], calories: 320, dietary: ["vegetarian", "gluten-free"] },
|
||||
{ en: "Spaghetti Carbonara", ru: "Спагетти Карбонара", cuisine: "italian", type: "dinner", difficulty: "medium", time: 20, desc: "Classic Roman pasta with eggs, pecorino, guanciale and black pepper.", ruDesc: "Классическая римская паста с яйцами, пекорино, гуанчиале и черным перцем.", ingredients: ["spaghetti", "eggs", "pecorino", "guanciale", "black pepper"], calories: 650, dietary: [] },
|
||||
@@ -400,7 +404,9 @@ const dietaryLabelMap: Record<string, { en: string; ru: string; emoji: string }>
|
||||
}
|
||||
}
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const lang = getClientLang();
|
||||
const isRu = lang === "ru";
|
||||
const T = getClientT();
|
||||
|
||||
// DOM refs
|
||||
const typeSelect = document.getElementById("mg-type");
|
||||
@@ -529,7 +535,7 @@ const dietaryLabelMap: Record<string, { en: string; ru: string; emoji: string }>
|
||||
return;
|
||||
}
|
||||
|
||||
const meal = list[Math.floor(Math.random() * list.length)];
|
||||
const meal = list[randomInt(0, list.length - 1)];
|
||||
currentMeal = meal;
|
||||
|
||||
// Name
|
||||
@@ -602,13 +608,13 @@ const dietaryLabelMap: Record<string, { en: string; ru: string; emoji: string }>
|
||||
|
||||
copyIcon.classList.add("hidden");
|
||||
checkIcon.classList.remove("hidden");
|
||||
copyText.textContent = isRu ? "Скопировано" : "Copied";
|
||||
copyText.textContent = T.copied;
|
||||
|
||||
if (copyTimer) clearTimeout(copyTimer);
|
||||
copyTimer = setTimeout(() => {
|
||||
checkIcon.classList.add("hidden");
|
||||
copyIcon.classList.remove("hidden");
|
||||
copyText.textContent = isRu ? "Копировать" : "Copy";
|
||||
copyText.textContent = T.copy;
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
@@ -674,5 +680,4 @@ const dietaryLabelMap: Record<string, { en: string; ru: string; emoji: string }>
|
||||
// Init
|
||||
updateSavedCount();
|
||||
generate();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -118,8 +118,10 @@ const genderOptions = [
|
||||
<script>
|
||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||
import { popElement } from "@/lib/client/animations";
|
||||
import { randomInt } from "@/lib/client/random";
|
||||
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const isRu = getClientLang() === "ru";
|
||||
|
||||
const EN_MALE = [
|
||||
"James", "John", "Robert", "Michael", "William", "David", "Richard",
|
||||
@@ -195,7 +197,7 @@ const genderOptions = [
|
||||
|
||||
function generate() {
|
||||
const pool = getNamesPool(getSelectedGender());
|
||||
const name = pool[Math.floor(Math.random() * pool.length)];
|
||||
const name = pool[randomInt(0, pool.length - 1)];
|
||||
resultEl.textContent = name;
|
||||
copyBtn.classList.remove("invisible");
|
||||
popElement(resultEl);
|
||||
|
||||
@@ -115,14 +115,10 @@ const T = useT(isRu ? "ru" : "en");
|
||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||
import { createErrorDisplay, isInteger } from "@/lib/client/validation";
|
||||
import { popElement } from "@/lib/client/animations";
|
||||
import { randomInt } from "@/lib/client/random";
|
||||
import { getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const ERR_INTEGERS = isRu
|
||||
? "Оба значения должны быть целыми числами."
|
||||
: "Both values must be whole numbers.";
|
||||
const ERR_FROM_TO = isRu
|
||||
? "«От» должно быть меньше «До»."
|
||||
: '"From" must be less than "To".';
|
||||
const T = getClientT();
|
||||
|
||||
const fromInput = document.getElementById("ng-from") as HTMLInputElement;
|
||||
const toInput = document.getElementById("ng-to") as HTMLInputElement;
|
||||
@@ -141,17 +137,17 @@ const T = useT(isRu ? "ru" : "en");
|
||||
|
||||
function generate() {
|
||||
if (!isInteger(fromInput.value) || !isInteger(toInput.value)) {
|
||||
errors.show(ERR_INTEGERS);
|
||||
errors.show(T.errBothIntegers);
|
||||
return;
|
||||
}
|
||||
const min = parseInt(fromInput.value, 10);
|
||||
const max = parseInt(toInput.value, 10);
|
||||
if (min >= max) {
|
||||
errors.show(ERR_FROM_TO);
|
||||
errors.show(T.errFromLessThanTo);
|
||||
return;
|
||||
}
|
||||
errors.clear();
|
||||
const result = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const result = randomInt(min, max);
|
||||
resultEl.textContent = String(result);
|
||||
copyBtn.classList.remove("invisible");
|
||||
popElement(resultEl);
|
||||
|
||||
@@ -99,8 +99,11 @@ const i18n = {
|
||||
|
||||
<script>
|
||||
import { createErrorDisplay } from "@/lib/client/validation";
|
||||
import { randomInt } from "@/lib/client/random";
|
||||
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const lang = getClientLang();
|
||||
const T = getClientT();
|
||||
|
||||
const harmonySelect = document.getElementById("pg-harmony") as HTMLSelectElement;
|
||||
const countSelect = document.getElementById("pg-count") as HTMLSelectElement;
|
||||
@@ -115,10 +118,6 @@ const i18n = {
|
||||
const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
||||
const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="text-accent"><path d="M20 6 9 17l-5-5"/></svg>`;
|
||||
|
||||
function randomInt(max: number): number {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
function hslToHex(h: number, s: number, l: number): string {
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
@@ -136,18 +135,18 @@ const i18n = {
|
||||
function generateRandomColors(count: number): string[] {
|
||||
const colors: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const h = randomInt(360);
|
||||
const s = 50 + randomInt(51);
|
||||
const l = 40 + randomInt(41);
|
||||
const h = randomInt(0, 359);
|
||||
const s = randomInt(50, 100);
|
||||
const l = randomInt(40, 80);
|
||||
colors.push(hslToHex(h, s, l));
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
function generateAnalogousColors(count: number): string[] {
|
||||
const baseH = randomInt(360);
|
||||
const s = 55 + randomInt(41);
|
||||
const l = 40 + randomInt(31);
|
||||
const baseH = randomInt(0, 359);
|
||||
const s = randomInt(55, 95);
|
||||
const l = randomInt(40, 70);
|
||||
const colors: string[] = [];
|
||||
const step = 30;
|
||||
const start = baseH - ((count - 1) * step) / 2;
|
||||
@@ -159,9 +158,9 @@ const i18n = {
|
||||
}
|
||||
|
||||
function generateComplementaryColors(count: number): string[] {
|
||||
const baseH = randomInt(360);
|
||||
const s = 55 + randomInt(41);
|
||||
const l = 40 + randomInt(31);
|
||||
const baseH = randomInt(0, 359);
|
||||
const s = randomInt(55, 95);
|
||||
const l = randomInt(40, 70);
|
||||
const colors: string[] = [];
|
||||
const compH = (baseH + 180) % 360;
|
||||
if (count <= 2) {
|
||||
@@ -178,9 +177,9 @@ const i18n = {
|
||||
}
|
||||
|
||||
function generateTriadicColors(count: number): string[] {
|
||||
const baseH = randomInt(360);
|
||||
const s = 55 + randomInt(41);
|
||||
const l = 40 + randomInt(31);
|
||||
const baseH = randomInt(0, 359);
|
||||
const s = randomInt(55, 95);
|
||||
const l = randomInt(40, 70);
|
||||
const colors: string[] = [];
|
||||
const triad = [baseH, (baseH + 120) % 360, (baseH + 240) % 360];
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -192,8 +191,8 @@ const i18n = {
|
||||
}
|
||||
|
||||
function generateMonochromaticColors(count: number): string[] {
|
||||
const baseH = randomInt(360);
|
||||
const s = 55 + randomInt(41);
|
||||
const baseH = randomInt(0, 359);
|
||||
const s = randomInt(55, 95);
|
||||
const colors: string[] = [];
|
||||
const startL = 25;
|
||||
const endL = 75;
|
||||
@@ -205,9 +204,9 @@ const i18n = {
|
||||
}
|
||||
|
||||
function generateSplitComplementaryColors(count: number): string[] {
|
||||
const baseH = randomInt(360);
|
||||
const s = 55 + randomInt(41);
|
||||
const l = 40 + randomInt(31);
|
||||
const baseH = randomInt(0, 359);
|
||||
const s = randomInt(55, 95);
|
||||
const l = randomInt(40, 70);
|
||||
const colors: string[] = [];
|
||||
const hues = [baseH, (baseH + 150) % 360, (baseH + 210) % 360];
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -219,9 +218,9 @@ const i18n = {
|
||||
}
|
||||
|
||||
function generateTetradicColors(count: number): string[] {
|
||||
const baseH = randomInt(360);
|
||||
const s = 55 + randomInt(41);
|
||||
const l = 40 + randomInt(31);
|
||||
const baseH = randomInt(0, 359);
|
||||
const s = randomInt(55, 95);
|
||||
const l = randomInt(40, 70);
|
||||
const colors: string[] = [];
|
||||
const rectHues = [baseH, (baseH + 60) % 360, (baseH + 180) % 360, (baseH + 240) % 360];
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -268,7 +267,7 @@ const i18n = {
|
||||
colorBox.className =
|
||||
"group relative w-20 h-20 sm:w-24 sm:h-24 rounded-2xl border border-zinc-800 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-transform duration-150 hover:scale-105";
|
||||
colorBox.style.backgroundColor = hex;
|
||||
colorBox.setAttribute("aria-label", isRu ? `Скопировать ${hex}` : `Copy ${hex}`);
|
||||
colorBox.setAttribute("aria-label", lang === "ru" ? `Скопировать ${hex}` : `Copy ${hex}`);
|
||||
colorBox.dataset.hex = hex;
|
||||
|
||||
// Copy overlay icon
|
||||
@@ -322,7 +321,7 @@ const i18n = {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
const originalText = copyAllBtn.textContent;
|
||||
copyAllBtn.textContent = isRu ? "Скопировано!" : "Copied!";
|
||||
copyAllBtn.textContent = T.copied + "!";
|
||||
setTimeout(() => {
|
||||
copyAllBtn.textContent = originalText;
|
||||
}, 1500);
|
||||
@@ -335,7 +334,7 @@ const i18n = {
|
||||
const colors = generatePalette(mode, count);
|
||||
renderSwatches(colors);
|
||||
} catch {
|
||||
errorDisplay.show(isRu ? "Не удалось сгенерировать палитру." : "Failed to generate palette.");
|
||||
errorDisplay.show(lang === "ru" ? "Не удалось сгенерировать палитру." : "Failed to generate palette.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,9 +132,11 @@ const T = useT(isRu ? "ru" : "en");
|
||||
<script>
|
||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||
import { createErrorDisplay } from "@/lib/client/validation";
|
||||
import { randomInt, shuffleArray } from "@/lib/client/random";
|
||||
import { getClientLang } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const ERR_SELECT_TYPE = isRu
|
||||
const lang = getClientLang();
|
||||
const ERR_SELECT_TYPE = lang === "ru"
|
||||
? "Выберите хотя бы один тип символов."
|
||||
: "Select at least one character type.";
|
||||
|
||||
@@ -178,17 +180,13 @@ const T = useT(isRu ? "ru" : "en");
|
||||
return;
|
||||
}
|
||||
errors.clear();
|
||||
const required = pools.map((p) => p[Math.floor(Math.random() * p.length)]);
|
||||
const required = pools.map((p) => p[randomInt(0, p.length - 1)]);
|
||||
const combined = pools.join("");
|
||||
const rest = Array.from(
|
||||
{ length: length - required.length },
|
||||
() => combined[Math.floor(Math.random() * combined.length)],
|
||||
() => combined[randomInt(0, combined.length - 1)],
|
||||
);
|
||||
const chars = [...required, ...rest];
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
const chars = shuffleArray([...required, ...rest]);
|
||||
resultEl.textContent = chars.join("");
|
||||
resultEl.style.opacity = "0.5";
|
||||
requestAnimationFrame(() =>
|
||||
|
||||
@@ -67,7 +67,10 @@ const T = useT(isRu ? "ru" : "en");
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
import { getClientLang } from "@/lib/client/i18n";
|
||||
import { randomInt } from "@/lib/client/random";
|
||||
|
||||
const isRu = getClientLang() === "ru";
|
||||
|
||||
const LABELS: Record<string, { name: string; emoji: string }> = isRu
|
||||
? {
|
||||
@@ -111,7 +114,7 @@ const T = useT(isRu ? "ru" : "en");
|
||||
}
|
||||
|
||||
function play(playerMove: string) {
|
||||
const botMove = MOVES[Math.floor(Math.random() * MOVES.length)];
|
||||
const botMove = MOVES[randomInt(0, MOVES.length - 1)];
|
||||
const result = getWinner(playerMove, botMove);
|
||||
|
||||
choices.forEach((btn) => {
|
||||
|
||||
@@ -112,13 +112,10 @@ const T = useT(isRu ? "ru" : "en");
|
||||
<script>
|
||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||
import { createErrorDisplay } from "@/lib/client/validation";
|
||||
import { shuffleArray } from "@/lib/client/random";
|
||||
import { getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const COPY_LABEL = isRu ? "Копировать" : "Copy";
|
||||
const COPIED_LABEL = isRu ? "Скопировано" : "Copied";
|
||||
const ERR_MIN_TWO = isRu
|
||||
? "Добавьте хотя бы 2 элемента."
|
||||
: "Add at least 2 items.";
|
||||
const T = getClientT();
|
||||
|
||||
const itemsInput = document.getElementById(
|
||||
"shf-items",
|
||||
@@ -143,15 +140,6 @@ const T = useT(isRu ? "ru" : "en");
|
||||
const errors = createErrorDisplay(errorEl);
|
||||
const clipboard = new CopyFeedback(copyIcon, checkIcon);
|
||||
|
||||
function fisherYatesShuffle<T>(array: T[]): T[] {
|
||||
const arr = [...array];
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function shuffle() {
|
||||
const items = itemsInput.value
|
||||
.split("\n")
|
||||
@@ -159,12 +147,12 @@ const T = useT(isRu ? "ru" : "en");
|
||||
.filter(Boolean);
|
||||
|
||||
if (items.length < 2) {
|
||||
errors.show(ERR_MIN_TWO);
|
||||
errors.show(T.errAddAtLeast2);
|
||||
return;
|
||||
}
|
||||
|
||||
errors.clear();
|
||||
lastShuffled = fisherYatesShuffle(items);
|
||||
lastShuffled = shuffleArray(items);
|
||||
|
||||
resultEl.innerHTML = "";
|
||||
lastShuffled.forEach((item, index) => {
|
||||
@@ -188,7 +176,7 @@ const T = useT(isRu ? "ru" : "en");
|
||||
});
|
||||
|
||||
resultArea.classList.remove("hidden");
|
||||
copyLabel.textContent = COPY_LABEL;
|
||||
copyLabel.textContent = T.copy;
|
||||
clipboard.revert();
|
||||
}
|
||||
|
||||
@@ -197,10 +185,10 @@ const T = useT(isRu ? "ru" : "en");
|
||||
await navigator.clipboard.writeText(lastShuffled.join("\n"));
|
||||
|
||||
clipboard.showCopied();
|
||||
copyLabel.textContent = COPIED_LABEL;
|
||||
copyLabel.textContent = T.copied;
|
||||
|
||||
setTimeout(() => {
|
||||
copyLabel.textContent = COPY_LABEL;
|
||||
copyLabel.textContent = T.copy;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user