mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-07-12 07:26:33 +02:00
549 lines
31 KiB
HTML
549 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Parseltongue 4.0 — Text Encoder, Decoder & Steganography Tool</title>
|
|
<meta name="description" content="Free online text encoder & decoder with 222+ transforms: ciphers, Base64, Unicode styles, emoji steganography & QR codes. No install required — because sometimes you need to speak in tongues that don't exist.">
|
|
<meta name="keywords" content="P4RS3LT0NGV3, Parseltongue, text encoder, text decoder, cipher tool, steganography, Unicode converter, Base64">
|
|
<meta name="application-name" content="P4RS3LT0NGV3">
|
|
<meta property="og:title" content="Parseltongue 4.0 — Text Encoder, Decoder & Steganography Tool">
|
|
<meta property="og:description" content="222+ encodings, ciphers & steganography in your browser. Because sometimes you need to speak in tongues that don't exist.">
|
|
<meta property="og:type" content="website">
|
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
<link rel="stylesheet" href="css/style.css">
|
|
<link rel="stylesheet" href="css/notification.css">
|
|
<!-- Vue.js (Production Build) -->
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
|
|
<!-- Font Awesome for icons -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
|
</head>
|
|
<body>
|
|
<script>
|
|
(function () {
|
|
try {
|
|
var saved = localStorage.getItem('theme') || 'dark';
|
|
document.body.classList.add('theme-' + saved);
|
|
document.body.classList.add(
|
|
saved === 'light' || saved === 'accessible' ? 'light-theme' : 'dark-theme'
|
|
);
|
|
} catch (e) {
|
|
document.body.classList.add('theme-dark', 'dark-theme');
|
|
}
|
|
})();
|
|
</script>
|
|
<div id="app" class="app-root">
|
|
<header class="app-topbar">
|
|
<div class="logo app-logo">
|
|
<h1>🐉 P4RS3LT0NGV3</h1>
|
|
</div>
|
|
<div class="actions topbar-actions">
|
|
<button
|
|
type="button"
|
|
class="topbar-utility-btn"
|
|
@click="mobileUtilityOpen ? closeMobileUtility() : openMobileUtility()"
|
|
:title="mobileUtilityOpen ? 'Close panels' : 'Open panels'"
|
|
:aria-label="mobileUtilityOpen ? 'Close panels' : 'Open panels'"
|
|
:aria-expanded="mobileUtilityOpen"
|
|
>
|
|
<i :class="mobileUtilityOpen ? 'fas fa-xmark' : 'fas fa-columns'"></i>
|
|
</button>
|
|
<a
|
|
href="https://github.com/elder-plinius/P4RS3LT0NGV3"
|
|
target="_blank"
|
|
class="github-button"
|
|
title="View source on GitHub"
|
|
aria-label="View source code on GitHub"
|
|
>
|
|
<i class="fab fa-github"></i>
|
|
</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="app-shell">
|
|
<nav class="app-nav" role="tablist" aria-label="Tools">
|
|
<div class="tab-buttons">
|
|
<button
|
|
v-for="tool in registeredTools"
|
|
v-if="!tool.hidden"
|
|
:key="tool.id"
|
|
:class="{ active: activeTab === tool.id }"
|
|
@click="switchToTab(tool.id)"
|
|
:title="tool.title"
|
|
role="tab"
|
|
:aria-selected="activeTab === tool.id"
|
|
>
|
|
<i :class="'fas ' + tool.icon"></i>
|
|
<span class="nav-label">{{ tool.name }}</span>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="app-main">
|
|
<div class="tab-tool-select">
|
|
<label for="mobile-tool-select">Selected Tool</label>
|
|
<select
|
|
id="mobile-tool-select"
|
|
class="mobile-tool-dropdown"
|
|
:value="activeTab"
|
|
aria-label="Selected tool"
|
|
@change="switchToTab($event.target.value)"
|
|
>
|
|
<template v-for="tool in registeredTools">
|
|
<option
|
|
v-if="!tool.hidden"
|
|
:key="tool.id"
|
|
:value="tool.id"
|
|
>{{ tool.name }}</option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="tool-content-container" class="tool-content-container">
|
|
</div>
|
|
</main>
|
|
|
|
<div
|
|
v-if="mobileUtilityOpen"
|
|
class="utility-backdrop"
|
|
@click="closeMobileUtility"
|
|
aria-hidden="true"
|
|
></div>
|
|
|
|
<aside
|
|
class="app-utility"
|
|
:class="{ 'utility-open': mobileUtilityOpen }"
|
|
aria-label="Utilities"
|
|
@click.stop
|
|
>
|
|
<div class="utility-tab-bar" role="tablist" aria-label="Utility panels" @click.stop>
|
|
<button
|
|
type="button"
|
|
class="utility-tab-btn"
|
|
:class="{ active: activeUtilityPanel === 'history' }"
|
|
@click.stop="switchUtilityPanel('history')"
|
|
role="tab"
|
|
:aria-selected="activeUtilityPanel === 'history'"
|
|
>
|
|
Copy History
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="utility-tab-btn"
|
|
:class="{ active: activeUtilityPanel === 'glitch' }"
|
|
@click.stop="switchUtilityPanel('glitch')"
|
|
role="tab"
|
|
:aria-selected="activeUtilityPanel === 'glitch'"
|
|
>
|
|
Glitch Tokens
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="utility-tab-btn"
|
|
:class="{ active: activeUtilityPanel === 'endsequences' }"
|
|
@click.stop="switchUtilityPanel('endsequences')"
|
|
role="tab"
|
|
:aria-selected="activeUtilityPanel === 'endsequences'"
|
|
>
|
|
End Sequences
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="utility-tab-btn"
|
|
:class="{ active: activeUtilityPanel === 'settings' }"
|
|
@click.stop="switchUtilityPanel('settings')"
|
|
role="tab"
|
|
:aria-selected="activeUtilityPanel === 'settings'"
|
|
>
|
|
Settings
|
|
</button>
|
|
</div>
|
|
|
|
<div class="utility-panels">
|
|
<div v-show="activeUtilityPanel === 'history'" class="utility-panel copy-history-panel">
|
|
<div class="utility-panel-toolbar" v-if="copyHistory.length > 0">
|
|
<button
|
|
@click.stop="clearCopyHistory"
|
|
class="clear-history-button"
|
|
title="Clear all history"
|
|
>
|
|
<i class="fas fa-trash"></i> Clear all
|
|
</button>
|
|
</div>
|
|
<div class="utility-panel-body copy-history-content">
|
|
<div v-if="copyHistory.length === 0" class="no-history">
|
|
<p>No copy history yet. Use the app features to auto-copy content.</p>
|
|
</div>
|
|
<div v-else class="history-items">
|
|
<div v-for="(item, index) in copyHistory" :key="item.id || index" class="history-item">
|
|
<div class="history-item-header">
|
|
<span class="history-source">{{ item.source }}</span>
|
|
<span class="history-time">{{ formatHistoryTime(item.timestamp) }}</span>
|
|
</div>
|
|
<div class="history-content">
|
|
{{ item.content }}
|
|
</div>
|
|
<div class="history-actions">
|
|
<button class="copy-again-button" @click="copyToClipboard(item.content)" title="Copy again">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
<button class="remove-history-button" @click.stop="removeFromCopyHistory(item.id)" title="Remove from history">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-show="activeUtilityPanel === 'glitch'" class="utility-panel glitch-token-panel">
|
|
<div class="utility-panel-body glitch-token-content">
|
|
<!-- Filter Section -->
|
|
<div class="glitch-token-filters">
|
|
<div class="filter-group">
|
|
<label>Behavior:</label>
|
|
<select v-model="glitchTokenBehavior" @change="filterGlitchTokens">
|
|
<option value="">All Behaviors</option>
|
|
<option value="UNSPEAKABLE">UNSPEAKABLE</option>
|
|
<option value="POLYSEMANTIC">POLYSEMANTIC</option>
|
|
<option value="GLITCHED_SPELLING">GLITCHED_SPELLING</option>
|
|
<option value="CONTEXT_CORRUPTOR">CONTEXT_CORRUPTOR</option>
|
|
<option value="LOOP_INDUCER">LOOP_INDUCER</option>
|
|
<option value="IDENTITY_DISRUPTOR">IDENTITY_DISRUPTOR</option>
|
|
<option value="FRAGMENT">FRAGMENT</option>
|
|
<option value="CONTROL_CHARACTER">CONTROL_CHARACTER</option>
|
|
<option value="SPECIAL_TOKEN">SPECIAL_TOKEN</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label>Search:</label>
|
|
<input
|
|
type="text"
|
|
v-model="glitchTokenSearch"
|
|
@input="filterGlitchTokens"
|
|
placeholder="Search tokens..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tokens List -->
|
|
<div class="glitch-token-list">
|
|
<div v-if="filteredGlitchTokens.length === 0" class="no-tokens">
|
|
<p v-if="!glitchTokensLoaded">Loading glitch tokens...</p>
|
|
<div v-else class="empty-state">
|
|
<p><strong>No glitch tokens available.</strong></p>
|
|
<p>Glitch token data is not bundled by default. To use this feature:</p>
|
|
<ol style="text-align: left; margin: 10px 0; padding-left: 20px;">
|
|
<li>Obtain glitch token data (e.g., from AGGREGLITCH library)</li>
|
|
<li>Open browser console and run:<br/>
|
|
<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">window.setGlitchTokensData(yourData)</code>
|
|
</li>
|
|
<li>Refresh the panel to see tokens</li>
|
|
</ol>
|
|
<p style="font-size: 0.9em; color: #666; margin-top: 10px;">
|
|
The data structure should match the AGGREGLITCH format with <code>glitch_tokens</code> containing categorized token arrays.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div v-else class="token-cards">
|
|
<div
|
|
v-for="(token, index) in filteredGlitchTokens"
|
|
:key="index"
|
|
class="token-card"
|
|
:class="'behavior-' + (token.behavior || '').toLowerCase()"
|
|
>
|
|
<div class="token-card-header">
|
|
<span class="token-text">{{ token.token || 'N/A' }}</span>
|
|
<button
|
|
class="copy-token-button"
|
|
@click="copyGlitchToken(token.token)"
|
|
title="Copy token"
|
|
>
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
<div class="token-card-body">
|
|
<div class="token-badge" :class="'badge-' + (token.behavior || '').toLowerCase()">
|
|
{{ token.behavior || 'Unknown' }}
|
|
</div>
|
|
<div v-if="token.token_id" class="token-id">
|
|
ID: {{ token.token_id }}
|
|
</div>
|
|
<div v-if="token.origin" class="token-origin">
|
|
Origin: {{ token.origin }}
|
|
</div>
|
|
<div v-if="token.observed_output" class="token-output">
|
|
Observed: {{ token.observed_output }}
|
|
</div>
|
|
<div v-if="token.note" class="token-note">
|
|
{{ token.note }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-show="activeUtilityPanel === 'endsequences'" class="utility-panel end-sequence-panel">
|
|
<div class="utility-panel-body end-sequence-content">
|
|
<p class="end-sequence-lede">
|
|
Strings sometimes used to probe delimiter and termination behavior. Copy into your payloads as needed for authorized testing.
|
|
</p>
|
|
<div
|
|
v-for="cat in endSequenceCategories"
|
|
:key="cat.title"
|
|
class="endsequence-category"
|
|
>
|
|
<h4>{{ cat.title }}</h4>
|
|
<div class="endsequence-items">
|
|
<button
|
|
v-for="(item, idx) in cat.items"
|
|
:key="cat.title + '-' + idx"
|
|
type="button"
|
|
class="endsequence-item"
|
|
@click="copyEndSequence(item.value)"
|
|
:title="'Copy to clipboard'"
|
|
:aria-label="'Copy ' + item.label"
|
|
>
|
|
<code class="endsequence-label">{{ item.label }}</code>
|
|
<span class="endsequence-copy-affordance" aria-hidden="true">
|
|
<i class="fas fa-copy"></i>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-show="activeUtilityPanel === 'settings'" id="unicode-options-panel" class="utility-panel unicode-options-panel">
|
|
<div class="utility-panel-body unicode-panel-content">
|
|
<div class="settings-section theme-settings-section">
|
|
<h4><i class="fas fa-palette"></i> Theme</h4>
|
|
<small class="settings-hint">App appearance. Press <kbd>D</kbd> to cycle themes.</small>
|
|
<label class="settings-theme-field">
|
|
Active theme
|
|
<select
|
|
class="settings-theme-select"
|
|
:value="activeTheme"
|
|
@change="setTheme($event.target.value)"
|
|
aria-label="Select theme"
|
|
>
|
|
<option
|
|
v-for="theme in themeOptions"
|
|
:key="theme.id"
|
|
:value="theme.id"
|
|
>
|
|
{{ theme.name }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<hr class="settings-divider" />
|
|
<div class="settings-section api-key-section">
|
|
<h4><i class="fas fa-key"></i> OpenRouter API Key</h4>
|
|
<small class="settings-hint">Required for Translation, PromptCraft, and Anti-Classifier. Stored locally in your browser only.</small>
|
|
<div class="api-key-input-row">
|
|
<input
|
|
:type="showApiKey ? 'text' : 'password'"
|
|
v-model="openrouterApiKey"
|
|
placeholder="sk-or-..."
|
|
class="api-key-input"
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
/>
|
|
<button class="api-key-toggle" @click="showApiKey = !showApiKey" :title="showApiKey ? 'Hide key' : 'Show key'">
|
|
<i :class="showApiKey ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
|
</button>
|
|
</div>
|
|
<div class="api-key-actions">
|
|
<button class="api-key-save" @click="saveApiKey" :disabled="!openrouterApiKey">
|
|
<i class="fas fa-save"></i> Save Key
|
|
</button>
|
|
<button class="api-key-clear" @click="clearApiKey" v-if="openrouterApiKey">
|
|
<i class="fas fa-trash"></i> Clear
|
|
</button>
|
|
<small v-if="apiKeySaved" class="apply-status">Saved</small>
|
|
</div>
|
|
</div>
|
|
<hr class="settings-divider" />
|
|
<div class="settings-section openrouter-models-section">
|
|
<h4><i class="fas fa-robot"></i> AI Models</h4>
|
|
<small class="settings-hint">Choose which models appear in tool dropdowns. Disabled models stay hidden unless currently selected.</small>
|
|
<div class="openrouter-models-toolbar">
|
|
<input
|
|
type="search"
|
|
v-model="openRouterModelsFilterQuery"
|
|
class="openrouter-models-search"
|
|
placeholder="Search models…"
|
|
autocomplete="off"
|
|
/>
|
|
<button type="button" class="openrouter-models-action" @click="enableAllOpenRouterModels">Show all</button>
|
|
<button type="button" class="openrouter-models-action" @click="showFreeOpenRouterModelsOnly">Free only</button>
|
|
<button type="button" class="openrouter-models-action" @click="refreshOpenRouterModels(true)" :disabled="openRouterModelsLoading">
|
|
<i class="fas" :class="openRouterModelsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'"></i>
|
|
</button>
|
|
</div>
|
|
<small class="openrouter-models-summary">
|
|
{{ openRouterModels.length }} shown in dropdowns · {{ openRouterModelsCatalog.length }} loaded
|
|
<span v-if="openRouterModelsDisabled.length"> · {{ openRouterModelsDisabled.length }} hidden</span>
|
|
</small>
|
|
<small v-if="openRouterModelsError" class="openrouter-model-hint openrouter-model-hint-error">{{ openRouterModelsError }}</small>
|
|
<div class="openrouter-models-list" role="group" aria-label="Model visibility">
|
|
<label
|
|
v-for="model in filteredOpenRouterModelsCatalog"
|
|
:key="model.id"
|
|
class="openrouter-model-toggle-row"
|
|
:class="{ 'is-virtual-router': model.virtual }"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
:checked="isOpenRouterModelEnabled(model.id)"
|
|
@change="toggleOpenRouterModelEnabled(model.id)"
|
|
/>
|
|
<span class="openrouter-model-toggle-text">
|
|
<span class="openrouter-model-toggle-name">{{ formatOpenRouterModelLabel(model) }}</span>
|
|
<small v-if="model.summary" class="openrouter-model-toggle-summary">{{ model.summary }}</small>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<hr class="settings-divider" />
|
|
<h4><i class="fas fa-user-secret"></i> Emoji Steganography</h4>
|
|
<small class="settings-hint">Global encoding options for the Steganography tool (emoji carrier bit patterns). Not used by Unicode style transforms.</small>
|
|
<div class="options-grid steg-adv-panel">
|
|
<label>
|
|
Initial Presentation
|
|
<select class="steg-initial-presentation">
|
|
<option value="emoji">Emoji (VS16)</option>
|
|
<option value="text">Text (VS15)</option>
|
|
<option value="none">None</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Bit-0 Selector
|
|
<select class="steg-vs-zero">
|
|
<option value="\ufe0e">VS15 (\ufe0e)</option>
|
|
<option value="\ufe0f">VS16 (\ufe0f)</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Bit-1 Selector
|
|
<select class="steg-vs-one">
|
|
<option value="\ufe0f">VS16 (\ufe0f)</option>
|
|
<option value="\ufe0e">VS15 (\ufe0e)</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Inter-bit Zero-Width
|
|
<select class="steg-inter-zw">
|
|
<option value="">None</option>
|
|
<option value="\u200C">ZWNJ (\u200C)</option>
|
|
<option value="\u200D">ZWJ (\u200D)</option>
|
|
<option value="\u200B">ZWSP (\u200B)</option>
|
|
<option value="\ufeff">BOM (\ufeff)</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Inter-bit Every N bits
|
|
<input class="steg-inter-every" type="number" min="1" max="8" value="1" />
|
|
</label>
|
|
<label>
|
|
Bit Order
|
|
<select class="steg-bit-order">
|
|
<option value="msb">MSB First</option>
|
|
<option value="lsb">LSB First</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Trailing Zero-Width
|
|
<select class="steg-trailing-zw">
|
|
<option value="\u200B">ZWSP (\u200B)</option>
|
|
<option value="\u200C">ZWNJ (\u200C)</option>
|
|
<option value="\u200D">ZWJ (\u200D)</option>
|
|
<option value="\ufeff">BOM (\ufeff)</option>
|
|
<option value="">None</option>
|
|
</select>
|
|
</label>
|
|
<div style="display:flex; align-items:center; gap:10px;">
|
|
<button class="apply-steg-options" :class="{ applied: unicodeApplyFlash }" :disabled="unicodeApplyBusy" @click="applyUnicodeOptions" title="These options affect Unicode-based steganography encoding/decoding.">Apply</button>
|
|
<small v-if="unicodeApplyFlash" class="apply-status">Applied</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
|
|
</div>
|
|
<!-- End of #app div -->
|
|
|
|
<!-- Load JavaScript files after Vue template -->
|
|
<!-- Data files (generated/static data) -->
|
|
<script src="js/data/emojiData.js"></script>
|
|
<script src="js/data/emojiCompatibility.js"></script>
|
|
|
|
<!-- Generated bundles -->
|
|
<script src="js/bundles/transforms-bundle.js"></script>
|
|
<script src="js/core/spellingAlphabetTransform.js"></script>
|
|
<script src="js/core/customSpellingAlphabets.js"></script>
|
|
|
|
<!-- Glitch Tokens Data -->
|
|
<script src="js/data/glitchTokens.js"></script>
|
|
<script src="js/data/endSequences.js"></script>
|
|
<script src="js/data/openrouterModels.js"></script>
|
|
<script src="js/data/anticlassifierPrompt.js"></script>
|
|
<script src="js/data/latinAffixPolicies.js"></script>
|
|
|
|
<!-- Load Configuration and Utilities (before modules that depend on them) -->
|
|
<script src="js/config/constants.js"></script>
|
|
<script src="js/utils/escapeParser.js"></script>
|
|
<script src="js/utils/glitchTokens.js"></script>
|
|
<script src="js/utils/focus.js"></script>
|
|
<script src="js/utils/notifications.js"></script>
|
|
<script src="js/utils/history.js"></script>
|
|
<script src="js/utils/clipboard.js"></script>
|
|
<script src="js/utils/theme.js"></script>
|
|
<script src="js/utils/openrouterModels.js"></script>
|
|
<script src="js/utils/routing.js"></script>
|
|
<script src="js/utils/emoji.js"></script>
|
|
<script src="js/utils/ean13.js"></script>
|
|
|
|
<!-- Core modules (feature libraries) -->
|
|
<script src="js/core/steganography.js"></script>
|
|
<script src="js/core/transformOptions.js"></script>
|
|
<script src="js/core/decoder.js"></script>
|
|
<script src="js/core/lexemeAnalysis.js"></script>
|
|
<script src="js/core/transformSelect.js"></script>
|
|
|
|
<!-- Load Tool System -->
|
|
<script src="js/tools/Tool.js"></script>
|
|
<script src="js/tools/AntiClassifierTool.js"></script>
|
|
<script src="js/tools/BijectionTool.js"></script>
|
|
<script src="js/tools/CodesTool.js"></script>
|
|
<script src="js/tools/DecodeTool.js"></script>
|
|
<script src="js/tools/EmojiTool.js"></script>
|
|
<script src="js/tools/GibberishTool.js"></script>
|
|
<script src="js/tools/MutationTool.js"></script>
|
|
<script src="js/tools/PromptCraftTool.js"></script>
|
|
<script src="js/tools/SpellingAlphabetTool.js"></script>
|
|
<script src="js/tools/SplitterTool.js"></script>
|
|
<script src="js/tools/TokenadeTool.js"></script>
|
|
<script src="js/tools/TokenizerTool.js"></script>
|
|
<script src="js/tools/TransformTool.js"></script>
|
|
<script src="js/tools/TranslateTool.js"></script>
|
|
<script src="js/core/toolRegistry.js"></script>
|
|
|
|
<!-- Codes tool vendor libraries (bundled locally at build time) -->
|
|
<script src="js/vendor/qrcode.min.js"></script>
|
|
<script src="js/vendor/JsBarcode.min.js"></script>
|
|
<script src="js/vendor/zxing.min.js"></script>
|
|
|
|
<script src="js/app.js"></script>
|
|
|
|
<!-- UI Initialization is handled in app.js mounted() lifecycle hook -->
|
|
</body>
|
|
</html>
|
|
<!-- redeploy trigger: semaphore + tokenizer updates -->
|