feat(wiki): инфраструктура JS/Lua вкладок в статьях
Что сделано:
1. docsLang.jsx (НОВЫЙ):
- DocsLangProvider — Context для выбранного языка (localStorage).
- DocsLangPicker — большой переключатель JS/Lua над разделом.
- <LangTabs js={...} lua={...} /> — локальные вкладки внутри
статьи: показывает контент текущего языка.
- useDocsLang() хук.
- Стили для picker / tabs / langChoiceModal / docTable.
2. docsData.jsx:
- Новая статья D0 "Скриптинг: JS или Lua — что выбрать?"
в самом верху раздела D. Сравнение, примеры одного и того же
кода на двух языках, советы новичкам.
- Импорт LangTabs.
3. KubikonDocs.jsx:
- ChapterPage обёрнут в DocsLangProvider + DocsLangPicker сверху.
Юзер может одним кликом переключить весь раздел JS↔Lua.
- LessonPage: при «Открыть мою копию» теперь показывается модалка
LangChoiceModal (JS / Lua). Создаём копию с нужными скриптами.
- convertProjectScriptsToLua() конвертит project_data:
если в скрипте есть code_lua слот — активируем. Иначе ставим
stub с подсказкой.
4. docsGamesBuilders.js:
- buildGameProject(id, opts) принимает opts.lang='lua'.
Та же логика — code_lua или stub.
ОСТАЛОСЬ (постепенно):
- Lua-эквиваленты в существующих 78 статьях. Сейчас Picker уже
показывается, но если в статье нет <LangTabs> — контент одинаковый.
Будем добавлять <LangTabs> в ключевые места по очереди.
- Lua-версии в GAME_BUILDERS для уроков 1-50 (code_lua слот).
This commit is contained in:
parent
0805da0708
commit
d019da0ab6
@ -13,6 +13,7 @@ import { GAMES, GAME_GROUPS } from './docsGames';
|
|||||||
import { LESSONS, hasLesson } from './docsLessons';
|
import { LESSONS, hasLesson } from './docsLessons';
|
||||||
import { buildGameProject } from './docsGamesBuilders';
|
import { buildGameProject } from './docsGamesBuilders';
|
||||||
import DocIcon from './docsIcons';
|
import DocIcon from './docsIcons';
|
||||||
|
import { DocsLangProvider, DocsLangPicker, DOCS_LANG_STYLES, useDocsLang } from './docsLang';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KubikonDocs — вика редактора Рублокс.
|
* KubikonDocs — вика редактора Рублокс.
|
||||||
@ -76,6 +77,7 @@ const KubikonDocs = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={cl.studio}>
|
<div className={cl.studio}>
|
||||||
<style>{INLINE_STYLES}</style>
|
<style>{INLINE_STYLES}</style>
|
||||||
|
<style>{DOCS_LANG_STYLES}</style>
|
||||||
|
|
||||||
{/* === Левая боковая панель === */}
|
{/* === Левая боковая панель === */}
|
||||||
<aside className={cl.sidebar}>
|
<aside className={cl.sidebar}>
|
||||||
@ -383,12 +385,15 @@ const ChapterPage = ({ chapter, mainRef }) => {
|
|||||||
|
|
||||||
{/* Контент раздела */}
|
{/* Контент раздела */}
|
||||||
<div className="docsContent">
|
<div className="docsContent">
|
||||||
|
<DocsLangProvider>
|
||||||
|
<DocsLangPicker />
|
||||||
{chapter.sections.map((s) => (
|
{chapter.sections.map((s) => (
|
||||||
<article key={s.id} id={`sec-${s.id}`} className="docsChapter">
|
<article key={s.id} id={`sec-${s.id}`} className="docsChapter">
|
||||||
<h3 className="docsSectionTitle">{s.title}</h3>
|
<h3 className="docsSectionTitle">{s.title}</h3>
|
||||||
<div className="docsSectionBody">{s.body}</div>
|
<div className="docsSectionBody">{s.body}</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
|
</DocsLangProvider>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -399,17 +404,20 @@ const ChapterPage = ({ chapter, mainRef }) => {
|
|||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
const LessonPage = ({ game, navigate }) => {
|
const LessonPage = ({ game, navigate }) => {
|
||||||
const lesson = LESSONS[game.id];
|
const lesson = LESSONS[game.id];
|
||||||
// 'idle' | 'creating' | 'error'
|
// 'idle' | 'choosing' | 'creating' | 'error'
|
||||||
const [state, setState] = useState('idle');
|
const [state, setState] = useState('idle');
|
||||||
|
|
||||||
// Создаёт НОВУЮ копию игры-урока на текущем пользователе и
|
// Шаг 1: юзер нажал «Открыть копию» → показываем модалку выбора языка.
|
||||||
// открывает её в редакторе. Оригинал при этом ВСЕГДА цел.
|
const openInEditor = () => {
|
||||||
const openInEditor = async () => {
|
|
||||||
const userId = getCurrentUserId();
|
const userId = getCurrentUserId();
|
||||||
if (!userId) {
|
if (!userId) { setState('error'); return; }
|
||||||
setState('error');
|
setState('choosing');
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
// Шаг 2: язык выбран → создаём копию с нужными скриптами и открываем.
|
||||||
|
const createCopyWithLang = async (lang) => {
|
||||||
|
const userId = getCurrentUserId();
|
||||||
|
if (!userId) { setState('error'); return; }
|
||||||
setState('creating');
|
setState('creating');
|
||||||
try {
|
try {
|
||||||
// project_data копии берём двумя способами:
|
// project_data копии берём двумя способами:
|
||||||
@ -422,9 +430,11 @@ const LessonPage = ({ game, navigate }) => {
|
|||||||
const pd = orig && orig.data && orig.data.project_data;
|
const pd = orig && orig.data && orig.data.project_data;
|
||||||
if (!pd) { setState('error'); return; }
|
if (!pd) { setState('error'); return; }
|
||||||
// project_data может прийти строкой или объектом — нормализуем в строку.
|
// project_data может прийти строкой или объектом — нормализуем в строку.
|
||||||
projectDataStr = typeof pd === 'string' ? pd : JSON.stringify(pd);
|
let pdObj = typeof pd === 'string' ? JSON.parse(pd) : pd;
|
||||||
|
if (lang === 'lua') pdObj = convertProjectScriptsToLua(pdObj);
|
||||||
|
projectDataStr = JSON.stringify(pdObj);
|
||||||
} else {
|
} else {
|
||||||
const project = buildGameProject(game.id);
|
const project = buildGameProject(game.id, { lang });
|
||||||
if (!project) { setState('error'); return; }
|
if (!project) { setState('error'); return; }
|
||||||
projectDataStr = JSON.stringify(project);
|
projectDataStr = JSON.stringify(project);
|
||||||
}
|
}
|
||||||
@ -477,6 +487,12 @@ const LessonPage = ({ game, navigate }) => {
|
|||||||
: <><Icon name="play" size={15} /> Открыть мою копию в редакторе</>}
|
: <><Icon name="play" size={15} /> Открыть мою копию в редакторе</>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{state === 'choosing' && (
|
||||||
|
<LangChoiceModal
|
||||||
|
onPick={(lang) => createCopyWithLang(lang)}
|
||||||
|
onCancel={() => setState('idle')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{state === 'error' && (
|
{state === 'error' && (
|
||||||
<div className="lessonErr">
|
<div className="lessonErr">
|
||||||
Не получилось открыть игру. Проверь, что ты вошёл в аккаунт,
|
Не получилось открыть игру. Проверь, что ты вошёл в аккаунт,
|
||||||
@ -492,6 +508,81 @@ const LessonPage = ({ game, navigate }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// Модалка выбора языка скриптов при «Открыть копию»
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
const LangChoiceModal = ({ onPick, onCancel }) => {
|
||||||
|
return (
|
||||||
|
<div className="langChoiceOverlay" onClick={onCancel}>
|
||||||
|
<div className="langChoiceDialog" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h3 className="langChoiceTitle">На каком языке открыть копию?</h3>
|
||||||
|
<p className="langChoiceSub">
|
||||||
|
Скрипты в твоей копии будут написаны на выбранном языке.
|
||||||
|
Логика игры одинаковая — отличается только запись кода.
|
||||||
|
</p>
|
||||||
|
<div className="langChoiceBtns">
|
||||||
|
<button className="langChoiceBtn langChoiceBtn--js"
|
||||||
|
onClick={() => onPick('js')}>
|
||||||
|
<div className="langChoiceBtn__name">JavaScript</div>
|
||||||
|
<div className="langChoiceBtn__hint">
|
||||||
|
Если ты новичок — этот выбор проще.
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="langChoiceBtn langChoiceBtn--lua"
|
||||||
|
onClick={() => onPick('lua')}>
|
||||||
|
<div className="langChoiceBtn__name">Lua</div>
|
||||||
|
<div className="langChoiceBtn__hint">
|
||||||
|
Если играл в Roblox — узнаешь команды.
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button className="langChoiceCancel" onClick={onCancel}>Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует все JS-скрипты в project_data в Lua-эквивалент.
|
||||||
|
* Сейчас простая стратегия: если в скрипте есть code_lua слот, делает его
|
||||||
|
* активным. Иначе ставит флаг language='lua' и пустой Lua-шаблон с TODO.
|
||||||
|
* Полноценная транспиляция JS→Lua невозможна без AST-анализа.
|
||||||
|
*/
|
||||||
|
function convertProjectScriptsToLua(projectData) {
|
||||||
|
const scene = projectData?.scene;
|
||||||
|
if (!scene || !Array.isArray(scene.scripts)) return projectData;
|
||||||
|
scene.scripts = scene.scripts.map(s => {
|
||||||
|
if (s.language === 'lua') return s;
|
||||||
|
// Если уже есть готовый Lua-слот — используем его
|
||||||
|
if (s.code_lua && s.code_lua.trim()) {
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
language: 'lua',
|
||||||
|
code: s.code_lua,
|
||||||
|
code_js: s.code_js || s.code,
|
||||||
|
code_lua: s.code_lua,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Иначе ставим заглушку с подсказкой
|
||||||
|
const luaStub = `-- TODO: версия этого скрипта на Lua пока не готова.
|
||||||
|
-- Оригинальный JS-код сохранён ниже (переключи язык назад на JS в редакторе).
|
||||||
|
-- Доступные API: game:GetService("Players"), game.Workspace, script.Parent
|
||||||
|
--
|
||||||
|
-- Например, простой пример:
|
||||||
|
local Players = game:GetService("Players")
|
||||||
|
print("Привет от Lua-скрипта")
|
||||||
|
`;
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
language: 'lua',
|
||||||
|
code: luaStub,
|
||||||
|
code_js: s.code_js || s.code,
|
||||||
|
code_lua: luaStub,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return projectData;
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
// Инлайн-стили
|
// Инлайн-стили
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocIcon from './docsIcons';
|
import DocIcon from './docsIcons';
|
||||||
|
import { LangTabs } from './docsLang';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* docsData.jsx — контент вики редактора Рублокс (разделы A-J).
|
* docsData.jsx — контент вики редактора Рублокс (разделы A-J).
|
||||||
@ -1122,6 +1123,119 @@ game.gui.onSubmit(boxId, (text) => {
|
|||||||
title: 'Скрипты — основы',
|
title: 'Скрипты — основы',
|
||||||
summary: 'Самый важный раздел: что такое скрипт, переменные, события, таймеры.',
|
summary: 'Самый важный раздел: что такое скрипт, переменные, события, таймеры.',
|
||||||
sections: [
|
sections: [
|
||||||
|
{
|
||||||
|
id: 'js-or-lua',
|
||||||
|
title: 'D0. Скриптинг: JS или Lua — что выбрать?',
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
В Рублоксе можно писать скрипты <b>на двух языках</b>:
|
||||||
|
<b> JavaScript</b> и <b>Lua</b>. Оба работают одинаково
|
||||||
|
хорошо. Игра не отличает их между собой — внутри одного
|
||||||
|
проекта одни скрипты могут быть на JS, другие на Lua,
|
||||||
|
и они общаются между собой как будто это один язык.
|
||||||
|
</p>
|
||||||
|
<p><b>Чем они отличаются?</b></p>
|
||||||
|
<table className="docTable">
|
||||||
|
<thead>
|
||||||
|
<tr><th></th><th>JavaScript</th><th>Lua</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><b>Где ещё используется</b></td>
|
||||||
|
<td>Сайты, мобильные приложения, серверы.
|
||||||
|
Самый популярный язык в мире.</td>
|
||||||
|
<td>Roblox, World of Warcraft (моды),
|
||||||
|
многие игры. Простой и быстрый.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Главный API</b></td>
|
||||||
|
<td><code>game.*</code> (game.player,
|
||||||
|
game.log, game.scene)</td>
|
||||||
|
<td><code>game.*</code> в Roblox-стиле
|
||||||
|
(game:GetService("Players"), workspace)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Похож на</b></td>
|
||||||
|
<td>Roblox-LUA если знаешь Roblox</td>
|
||||||
|
<td>Roblox-Studio — те же команды</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Когда выбрать</b></td>
|
||||||
|
<td>Если планируешь делать сайты и приложения
|
||||||
|
— JS пригодится везде.</td>
|
||||||
|
<td>Если играешь в Roblox и видел там скрипты
|
||||||
|
— Lua тебе знаком.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p><b>Один и тот же пример на двух языках:</b></p>
|
||||||
|
<p>Когда игрок касается синего блока — печатаем «Привет».</p>
|
||||||
|
<LangTabs
|
||||||
|
js={<Code>{`// JS — глобальный скрипт
|
||||||
|
game.onTouch('синий-блок', (player) => {
|
||||||
|
game.log('Привет, ' + player.name + '!');
|
||||||
|
});`}</Code>}
|
||||||
|
lua={<Code>{`-- Lua — скрипт на самом блоке
|
||||||
|
local part = script.Parent
|
||||||
|
|
||||||
|
part.Touched:Connect(function(hit)
|
||||||
|
local player = game.Players:GetPlayerFromCharacter(hit.Parent)
|
||||||
|
if player then
|
||||||
|
print("Привет, " .. player.Name .. "!")
|
||||||
|
end
|
||||||
|
end)`}</Code>}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Видишь — оба варианта делают <b>одно и то же</b>. Но
|
||||||
|
запись отличается. JS короче для простых вещей через
|
||||||
|
<code>game.onTouch</code>, Lua даёт точный Roblox-стиль
|
||||||
|
через <code>:Connect</code> и события.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><b>Что выбирать новичку?</b></p>
|
||||||
|
<Note>
|
||||||
|
Если ты <b>совсем новичок</b> — бери <b>JavaScript</b>.
|
||||||
|
Команды <code>game.*</code> в JS короче и проще читать.
|
||||||
|
Большая часть уроков в этой вике написана с примерами
|
||||||
|
на JS — все они работают, просто копируй.
|
||||||
|
</Note>
|
||||||
|
<Note>
|
||||||
|
Если ты <b>уже играл в Roblox</b> и видел там скрипты
|
||||||
|
— бери <b>Lua</b>. Команды почти один в один как
|
||||||
|
в Roblox Studio: <code>game:GetService</code>,
|
||||||
|
<code>:Connect</code>, <code>workspace</code>,
|
||||||
|
<code>script.Parent</code>.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<p><b>Можно ли менять язык в одном скрипте?</b></p>
|
||||||
|
<p>
|
||||||
|
Да. В редакторе скрипта вверху есть две кнопки —
|
||||||
|
<b>JS</b> и <b>Lua</b>. Просто нажми на нужную.
|
||||||
|
Твой код на текущем языке <b>сохранится</b>, а
|
||||||
|
на другом языке откроется пустой шаблон или то, что
|
||||||
|
ты писал там раньше. Никогда ничего не теряется.
|
||||||
|
</p>
|
||||||
|
<Try>
|
||||||
|
Создай новый скрипт. Напиши пару строк на JS. Нажми
|
||||||
|
кнопку <b>Lua</b> — JS-код спрячется, появится
|
||||||
|
Lua-шаблон. Напиши там что-то. Нажми <b>JS</b>
|
||||||
|
обратно — твой JS-код вернётся. Магия!
|
||||||
|
</Try>
|
||||||
|
|
||||||
|
<p><b>А что под капотом?</b></p>
|
||||||
|
<p>
|
||||||
|
JS-скрипты исполняются в <code>WebWorker</code> —
|
||||||
|
это отдельный поток в браузере, чтобы скрипт не
|
||||||
|
тормозил саму игру. Lua-скрипты исполняются через
|
||||||
|
<b> wasmoon</b> — Lua-интерпретатор, скомпилированный
|
||||||
|
в WebAssembly. Оба варианта работают на любом
|
||||||
|
устройстве без установки.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'what-is-script',
|
id: 'what-is-script',
|
||||||
title: 'D1. Что такое скрипт и как его создать',
|
title: 'D1. Что такое скрипт и как его создать',
|
||||||
|
|||||||
@ -6158,8 +6158,37 @@ export function hasGameBuilder(id) {
|
|||||||
return typeof GAME_BUILDERS[id] === 'function';
|
return typeof GAME_BUILDERS[id] === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Построить project_data для игры-урока. Возвращает объект или null. */
|
/** Построить project_data для игры-урока. Возвращает объект или null.
|
||||||
export function buildGameProject(id) {
|
* opts.lang: 'js' (default) | 'lua' — на каком языке скрипты в копии.
|
||||||
|
*/
|
||||||
|
export function buildGameProject(id, opts = {}) {
|
||||||
const fn = GAME_BUILDERS[id];
|
const fn = GAME_BUILDERS[id];
|
||||||
return fn ? fn() : null;
|
if (!fn) return null;
|
||||||
|
const project = fn();
|
||||||
|
if (opts.lang === 'lua' && project) {
|
||||||
|
// Если в скрипте есть code_lua слот — делаем его активным.
|
||||||
|
// Иначе ставим stub с заметкой что Lua-версия в работе.
|
||||||
|
const scene = project.scene || {};
|
||||||
|
if (Array.isArray(scene.scripts)) {
|
||||||
|
scene.scripts = scene.scripts.map(s => {
|
||||||
|
if (s.language === 'lua') return s;
|
||||||
|
if (s.code_lua && s.code_lua.trim()) {
|
||||||
|
return { ...s, language: 'lua', code: s.code_lua, code_js: s.code_js || s.code };
|
||||||
|
}
|
||||||
|
const luaStub = `-- TODO: Lua-версия этого скрипта пока не готова.
|
||||||
|
-- Переключи язык на JS в редакторе (кнопка JS вверху), чтобы увидеть рабочий код.
|
||||||
|
-- Lua-API: game:GetService("Players"), workspace, script.Parent
|
||||||
|
print("Lua-скрипт запущен (заглушка)")
|
||||||
|
`;
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
language: 'lua',
|
||||||
|
code: luaStub,
|
||||||
|
code_js: s.code_js || s.code,
|
||||||
|
code_lua: luaStub,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return project;
|
||||||
}
|
}
|
||||||
|
|||||||
324
src/community/docsLang.jsx
Normal file
324
src/community/docsLang.jsx
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
/**
|
||||||
|
* docsLang.jsx — поддержка вкладок JS/Lua в статьях вики.
|
||||||
|
*
|
||||||
|
* Компоненты:
|
||||||
|
* <DocsLangProvider> — оборачивает страницу статьи, хранит выбранный язык
|
||||||
|
* в localStorage 'rublox.docs.lang' ('js' | 'lua').
|
||||||
|
* <DocsLangPicker /> — большой переключатель JS/Lua над статьёй.
|
||||||
|
* <LangTabs js lua /> — вкладка-переключатель внутри статьи. Показывает
|
||||||
|
* либо js, либо lua, согласно текущему языку.
|
||||||
|
* useDocsLang() — хук: возвращает {lang, setLang}.
|
||||||
|
*
|
||||||
|
* Если в статье нет ни одного <LangTabs> — она одинаково выглядит на обоих
|
||||||
|
* языках (общая теория, не зависящая от языка скриптов).
|
||||||
|
*/
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const LS_KEY = 'rublox.docs.lang';
|
||||||
|
const DEFAULT_LANG = 'js';
|
||||||
|
|
||||||
|
const DocsLangContext = createContext({
|
||||||
|
lang: DEFAULT_LANG,
|
||||||
|
setLang: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DocsLangProvider({ children }) {
|
||||||
|
const [lang, setLangState] = useState(() => {
|
||||||
|
try {
|
||||||
|
const v = localStorage.getItem(LS_KEY);
|
||||||
|
return v === 'lua' ? 'lua' : 'js';
|
||||||
|
} catch (_) {
|
||||||
|
return DEFAULT_LANG;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const setLang = (next) => {
|
||||||
|
const v = next === 'lua' ? 'lua' : 'js';
|
||||||
|
setLangState(v);
|
||||||
|
try { localStorage.setItem(LS_KEY, v); } catch (_) {}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
// Слушаем смену из других вкладок
|
||||||
|
const onStorage = (e) => {
|
||||||
|
if (e.key === LS_KEY && (e.newValue === 'js' || e.newValue === 'lua')) {
|
||||||
|
setLangState(e.newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('storage', onStorage);
|
||||||
|
return () => window.removeEventListener('storage', onStorage);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<DocsLangContext.Provider value={{ lang, setLang }}>
|
||||||
|
{children}
|
||||||
|
</DocsLangContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDocsLang() {
|
||||||
|
return useContext(DocsLangContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Большой переключатель над статьёй: «На каком языке смотреть код?» */
|
||||||
|
export function DocsLangPicker() {
|
||||||
|
const { lang, setLang } = useDocsLang();
|
||||||
|
return (
|
||||||
|
<div className="docsLangPicker">
|
||||||
|
<div className="docsLangPicker__label">
|
||||||
|
Язык скриптов в этой статье:
|
||||||
|
</div>
|
||||||
|
<div className="docsLangPicker__tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
'docsLangPicker__tab docsLangPicker__tab--js' +
|
||||||
|
(lang === 'js' ? ' is-active' : '')
|
||||||
|
}
|
||||||
|
onClick={() => setLang('js')}
|
||||||
|
>
|
||||||
|
JavaScript
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
'docsLangPicker__tab docsLangPicker__tab--lua' +
|
||||||
|
(lang === 'lua' ? ' is-active' : '')
|
||||||
|
}
|
||||||
|
onClick={() => setLang('lua')}
|
||||||
|
>
|
||||||
|
Lua
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="docsLangPicker__hint">
|
||||||
|
Не знаешь что выбрать? Смотри статью <b>D0. Скриптинг: JS или Lua?</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Локальный переключатель вкладок внутри статьи. Если js/lua —
|
||||||
|
* прямой контент (children), если на странице нет <DocsLangProvider> —
|
||||||
|
* показываем оба заголовками.
|
||||||
|
*
|
||||||
|
* Использование:
|
||||||
|
* <LangTabs
|
||||||
|
* js={<Code>game.log('Привет')</Code>}
|
||||||
|
* lua={<Code>print('Привет')</Code>}
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
export function LangTabs({ js, lua }) {
|
||||||
|
const { lang, setLang } = useDocsLang();
|
||||||
|
const hasJs = js !== undefined && js !== null;
|
||||||
|
const hasLua = lua !== undefined && lua !== null;
|
||||||
|
if (!hasJs && !hasLua) return null;
|
||||||
|
// Если есть только один язык — показываем без переключателя
|
||||||
|
if (hasJs && !hasLua) return <>{js}</>;
|
||||||
|
if (!hasJs && hasLua) return <>{lua}</>;
|
||||||
|
return (
|
||||||
|
<div className="docsLangTabs">
|
||||||
|
<div className="docsLangTabs__head">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={'docsLangTabs__tab' + (lang === 'js' ? ' is-active' : '')}
|
||||||
|
onClick={() => setLang('js')}
|
||||||
|
>
|
||||||
|
JS
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={'docsLangTabs__tab' + (lang === 'lua' ? ' is-active' : '')}
|
||||||
|
onClick={() => setLang('lua')}
|
||||||
|
>
|
||||||
|
Lua
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="docsLangTabs__body">
|
||||||
|
{lang === 'lua' ? lua : js}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DOCS_LANG_STYLES = `
|
||||||
|
.docsLangPicker {
|
||||||
|
background: linear-gradient(135deg, #1a1d2e 0%, #14172b 100%);
|
||||||
|
border: 1px solid #2a3050;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin: 16px 0 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.docsLangPicker__label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #c8cce0;
|
||||||
|
}
|
||||||
|
.docsLangPicker__tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.docsLangPicker__tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: #232842;
|
||||||
|
color: #aab0c8;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.docsLangPicker__tab:hover { background: #2a304f; color: #fff; }
|
||||||
|
.docsLangPicker__tab--js.is-active {
|
||||||
|
background: linear-gradient(135deg, #f7df1e 0%, #d4b500 100%);
|
||||||
|
color: #1a1a1c;
|
||||||
|
border-color: #d4b500;
|
||||||
|
}
|
||||||
|
.docsLangPicker__tab--lua.is-active {
|
||||||
|
background: linear-gradient(135deg, #2196f3 0%, #1565c0 100%);
|
||||||
|
color: #fff;
|
||||||
|
border-color: #1565c0;
|
||||||
|
}
|
||||||
|
.docsLangPicker__hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a90a8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docsLangTabs {
|
||||||
|
margin: 12px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #2a3050;
|
||||||
|
}
|
||||||
|
.docsLangTabs__head {
|
||||||
|
display: flex;
|
||||||
|
background: #181b2c;
|
||||||
|
border-bottom: 1px solid #2a3050;
|
||||||
|
}
|
||||||
|
.docsLangTabs__tab {
|
||||||
|
padding: 8px 18px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #888da6;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
.docsLangTabs__tab:hover { color: #c8cce0; }
|
||||||
|
.docsLangTabs__tab.is-active {
|
||||||
|
color: #fff;
|
||||||
|
border-bottom-color: #4a8bff;
|
||||||
|
background: #1f2338;
|
||||||
|
}
|
||||||
|
.docsLangTabs__body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.docsLangTabs__body > pre,
|
||||||
|
.docsLangTabs__body > .docCode { margin: 0; border-radius: 0; }
|
||||||
|
|
||||||
|
.docTable {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.docTable th, .docTable td {
|
||||||
|
border: 1px solid #2a3050;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.docTable th {
|
||||||
|
background: #1a1d2e;
|
||||||
|
color: #c8cce0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.docTable td:first-child {
|
||||||
|
background: #181b2c;
|
||||||
|
width: 25%;
|
||||||
|
color: #aab0c8;
|
||||||
|
}
|
||||||
|
.docTable code {
|
||||||
|
background: #0e1020;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.langChoiceOverlay {
|
||||||
|
position: fixed; inset: 0;
|
||||||
|
background: rgba(0,0,0,0.75);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
.langChoiceDialog {
|
||||||
|
background: #1a1d2e;
|
||||||
|
border: 1px solid #2a3050;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 28px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
.langChoiceTitle {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.langChoiceSub {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #aab0c8;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.langChoiceBtns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.langChoiceBtn {
|
||||||
|
padding: 18px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.langChoiceBtn--js {
|
||||||
|
background: linear-gradient(135deg, #f7df1e 0%, #d4b500 100%);
|
||||||
|
color: #1a1a1c;
|
||||||
|
}
|
||||||
|
.langChoiceBtn--js:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(247,223,30,0.3); }
|
||||||
|
.langChoiceBtn--lua {
|
||||||
|
background: linear-gradient(135deg, #2196f3 0%, #1565c0 100%);
|
||||||
|
}
|
||||||
|
.langChoiceBtn--lua:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(33,150,243,0.4); }
|
||||||
|
.langChoiceBtn__name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.langChoiceBtn__hint {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.langChoiceCancel {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #2a3050;
|
||||||
|
color: #aab0c8;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.langChoiceCancel:hover { background: #232842; color: #fff; }
|
||||||
|
`;
|
||||||
Loading…
x
Reference in New Issue
Block a user