first commit

This commit is contained in:
maher
2026-06-06 13:55:42 +02:00
commit a75adf3c39
2371 changed files with 511696 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{compose.yaml,compose.*.yaml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+38
View File
@@ -0,0 +1,38 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###
###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
###< symfony/routing ###
###> doctrine/doctrine-bundle ###
DATABASE_URL=mysql://root:root@172.28.2.1:3306/anyvideo?mariadb-10.4.34
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
+4
View File
@@ -0,0 +1,4 @@
###> symfony/framework-bundle ###
APP_SECRET=1f540f22d77c65ca6c8fc867a45a7eb3
###< symfony/framework-bundle ###
+10
View File
@@ -0,0 +1,10 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
+77
View File
@@ -0,0 +1,77 @@
sudo apt update && sudo apt install jq
//pour audio la meilleur qualité
yt-dlp -J "https://www.youtube.com/watch?v=udjFBEM85to" | jq '
.formats
| map(select(.vcodec=="none" and .acodec!="none"))
| max_by(.abr // 0)
| {id: .format_id, ext, abr, codec: .acodec,
size_mb: ((.filesize // .filesize_approx // 0)/1024/1024|floor)}'
///pour le vidéo pour chaque format la meilleur qualité
yt-dlp -J "https://www.youtube.com/watch?v=udjFBEM85to" | jq '
.formats
| map(select(.acodec=="none" and .vcodec!="none"))
| group_by(.height)
| map(max_by((.tbr // 0), (.filesize // .filesize_approx // 0)))
| sort_by(.height)
| map({
id: .format_id,
ext,
resolution,
fps,
vcodec,
size_mb: ((.filesize // .filesize_approx // 0)/1024/1024|floor)
})
'
yt-dlp -f 137+140 URL
//la taille en Mo
echo $(($(yt-dlp --print "%(filesize,filesize_approx)s" "https://www.youtube.com/watch?v=r8gPG3JrNhw") / 1024 / 1024))
yt-dlp --newline "https://www.4s.io/video/k2gW_G5Yku/amira_w_dofda3.html"
<?php
$url = $_POST['url'];
$cmd = "yt-dlp --newline " . escapeshellarg($url) . " 2>&1";
$handle = popen($cmd, "r");
$socket = fsockopen("127.0.0.1", 8080);
function ws_send($socket, $msg) {
fwrite($socket, $msg . "\n");
}
while (!feof($handle)) {
$line = fgets($handle);
if (preg_match('/(\d+\.?\d*)%/', $line, $m)) {
$data = json_encode([
"progress" => (float)$m[1],
"line" => trim($line)
]);
ws_send($socket, $data);
}
}
pclose($handle);
ws_send($socket, json_encode([
"progress" => 100,
"status" => "done"
]));
fclose($socket);
Executable
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};
+85
View File
@@ -0,0 +1,85 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/doctrine-bundle": "^3.2",
"doctrine/doctrine-migrations-bundle": "^4.0",
"doctrine/orm": "^3.6",
"symfony/asset": "7.4.*",
"symfony/console": "7.4.*",
"symfony/doctrine-messenger": "7.4.*",
"symfony/dotenv": "7.4.*",
"symfony/flex": "^2",
"symfony/form": "7.4.*",
"symfony/framework-bundle": "7.4.*",
"symfony/messenger": "7.4.*",
"symfony/mime": "7.4.*",
"symfony/process": "7.4.*",
"symfony/runtime": "7.4.*",
"symfony/security-bundle": "7.4.*",
"symfony/translation": "7.4.*",
"symfony/twig-bundle": "7.4.*",
"symfony/validator": "7.4.*",
"symfony/yaml": "7.4.*"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"bump-after-update": true,
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.4.*"
}
},
"require-dev": {
"symfony/maker-bundle": "^1.67",
"symfony/stopwatch": "7.4.*",
"symfony/web-profiler-bundle": "7.4.*"
}
}
Generated
+6401
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
];
+19
View File
@@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null
+11
View File
@@ -0,0 +1,11 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout
+46
View File
@@ -0,0 +1,46 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
orm:
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system
+6
View File
@@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false
+15
View File
@@ -0,0 +1,15 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true
#fragments: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file
+15
View File
@@ -0,0 +1,15 @@
framework:
messenger:
transports:
# 1. On définit le nom du transport
async:
# 2. On définit son DSN (en retrait)
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
# 3. On définit sa stratégie de retry (au même niveau que dsn)
retry_strategy:
max_retries: 0
routing:
# Tout message de ce type sera envoyé vers le transport 'async'
'App\Entity\UserDownloader': async
+3
View File
@@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true
+10
View File
@@ -0,0 +1,10 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(DEFAULT_URI)%'
when@prod:
framework:
router:
strict_requirements: null
+41
View File
@@ -0,0 +1,41 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\LoginAuthenticator
logout:
path: security_logout
remember_me:
secret: '%kernel.secret%' # required
always_remember_me: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/partner, roles: ROLE_PARTNER }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
+5
View File
@@ -0,0 +1,5 @@
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
providers:
+9
View File
@@ -0,0 +1,9 @@
twig:
default_path: '%kernel.project_dir%/templates'
form_themes:
- 'form_div_layout.html.twig' # Default
- 'bootstrap_5_layout.html.twig' # Activates Bootstrap 5 formatting
when@test:
twig:
strict_variables: true
+11
View File
@@ -0,0 +1,11 @@
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false
+13
View File
@@ -0,0 +1,13 @@
when@dev:
web_profiler:
toolbar: true
framework:
profiler:
collect_serializer_data: true
when@test:
framework:
profiler:
collect: false
collect_serializer_data: true
+5
View File
@@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}
+1399
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
# This file is the entry point to configure the routes of your app.
# Methods with the #[Route] attribute are automatically imported.
# See also https://symfony.com/doc/current/routing.html
# To list all registered routes, run the following command:
# bin/console debug:router
controllers:
resource: routing.controllers
+4
View File
@@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error
+3
View File
@@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service
+8
View File
@@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler
+23
View File
@@ -0,0 +1,23 @@
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
View File
+371
View File
@@ -0,0 +1,371 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
}
body {
background: linear-gradient(145deg, #f5f7fa 0%, #e9edf5 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: stretch;
/* ← CORRECTION ICI */
margin: 0;
}
/* ---------- BARRE HORIZONTALE TOUT EN HAUT ---------- */
.top-header-bar {
width: 100%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border-bottom: 1px solid #eef2ff;
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.96);
/* ← une seule déclaration */
}
.top-bar-container {
max-width: 1280px;
margin: 0 auto;
padding: 0.9rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
/* Section langue statique (sans JS) */
.static-lang {
background: #f1f4f9;
padding: 0.5rem 1.3rem;
border-radius: 3rem;
font-weight: 600;
font-size: 0.9rem;
color: #1e293b;
display: inline-flex;
align-items: center;
gap: 0.6rem;
letter-spacing: 0.2px;
}
.static-lang a {
text-decoration: none;
font-size: 23px;
margin-right: 20px;
}
.main-alert {
max-width: 1200px;
width: 100%;
padding: 2rem 0rem 0rem;
align-self: center;
/* Hauteur fixe pour réserver lespace, même vide */
min-height: 90px;
/* Ajustez selon la hauteur réelle de votre alerte */
}
/* Quand lalerte est absente ou vide, on cache son contenu mais on garde lespace */
.main-alert:empty {
visibility: hidden;
/* optionnel : pour masquer complètement tout contenu fantôme */
}
/* Conteneur principal (centré, avec marge supérieure) */
.main-container {
max-width: 1200px;
width: 100%;
background: #ffffff;
box-shadow: 0 25px 50px -10px rgba(0, 0, 0, 0.15);
border-radius: 2.5rem;
padding: 2rem 2rem 2.5rem;
margin: 1rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.5);
transition: all 0.2s ease;
align-self: center;
}
.logo-area {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.logo-icon {
font-size: 2.8rem;
color: #ef4444;
filter: drop-shadow(0 4px 6px rgba(239, 68, 68, 0.3));
}
.logo-text {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #1e293b, #4f46e5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.url-section {
background: #f8fafc;
border-radius: 2rem;
padding: 1.5rem;
margin: 1.8rem 0 1.5rem;
}
.url-input-area {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
textarea {
width: 100%;
border: 1px solid #e2e8f0;
border-radius: 1.2rem;
padding: 1rem;
font-size: 0.95rem;
resize: vertical;
min-height: 90px;
background: white;
outline: none;
transition: 0.2s;
}
textarea:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
}
.format-row {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
margin-top: 0.5rem;
}
select {
padding: 0.7rem 1.2rem;
border-radius: 2rem;
border: 1px solid #d1d5db;
background: white;
font-weight: 500;
cursor: pointer;
outline: none;
min-width: 180px;
}
.process-btn {
background: #0f172a;
border: none;
color: white;
font-weight: 700;
padding: 0.8rem 2.2rem;
border-radius: 3rem;
font-size: 1rem;
letter-spacing: 0.3px;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: 0.25s;
box-shadow: 0 10px 20px -8px rgba(15, 23, 42, 0.4);
margin-left: auto;
}
.process-btn:hover {
background: #1e293b;
transform: scale(1.02);
}
.process-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.user-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #dee2e6;
}
.username {
font-weight: 600;
color: #333;
}
.video-list {
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
max-height: 400px;
overflow-y: auto;
padding-right: 0.3rem;
}
.video-item {
background: white;
border-radius: 1.2rem;
padding: 1rem 1.2rem;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.04);
border: 1px solid #edf2f7;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.video-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.video-name {
font-weight: 600;
color: #1e293b;
word-break: break-word;
}
.progress-container {
width: 100%;
height: 8px;
background: #e9eef3;
border-radius: 20px;
overflow: hidden;
margin: 0.2rem 0;
}
.progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #4f46e5, #818cf8);
border-radius: 20px;
transition: width 0.2s ease;
}
.progress-fill.error {
background: #ef4444;
}
/* Styles pour la barre de progression en mode indéterminé */
.progress-container .progress-fill.indeterminate {
width: 100%;
background-image: linear-gradient(90deg,
#4f46e5 0%,
#818cf8 25%,
#4f46e5 50%,
#818cf8 75%,
#4f46e5 100%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite linear;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.status-area {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.status-text {
font-size: 0.9rem;
font-weight: 500;
}
.pending-text {
color: #64748b;
}
.error-text {
color: #dc2626;
font-weight: 600;
}
.download-btn {
background: #0f172a;
color: white;
border: none;
padding: 0.4rem 1.2rem;
border-radius: 2rem;
font-weight: 600;
font-size: 0.8rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.3rem;
transition: 0.2s;
}
.download-btn:hover {
background: #334155;
}
/* RESPONSIVE : barre horizontale adaptative */
@media (max-width: 768px) {
.top-bar-container {
padding: 0.8rem 1rem;
flex-direction: column;
align-items: stretch;
gap: 0.7rem;
}
.static-lang,
.demo-status {
justify-content: center;
width: 100%;
}
.format-row {
flex-direction: column;
align-items: stretch;
}
.process-btn {
margin-left: 0;
justify-content: center;
}
.main-container {
padding: 1.5rem;
margin: 1rem;
}
.logo-text {
font-size: 1.6rem;
}
}
@media (max-width: 480px) {
.top-bar-container {
padding: 0.7rem 0.9rem;
}
.static-lang,
.demo-status {
font-size: 0.8rem;
padding: 0.4rem 1rem;
}
}
+342
View File
@@ -0,0 +1,342 @@
/* ── Page header ── */
.history-page-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.8rem;
padding-bottom: 1.2rem;
border-bottom: 1px solid #eef2ff;
}
.history-page-title {
font-size: 1.55rem;
font-weight: 700;
color: #1e293b;
display: flex;
align-items: center;
gap: 0.7rem;
}
.history-page-title i {
color: #4f46e5;
font-size: 1.3rem;
}
.history-badge {
background: #eef2ff;
color: #4f46e5;
font-size: 0.75rem;
font-weight: 700;
padding: 0.25rem 0.75rem;
border-radius: 2rem;
}
/* ── Search / filter bar ── */
.history-toolbar {
display: flex;
align-items: center;
gap: 0.8rem;
flex-wrap: wrap;
margin-bottom: 1.2rem;
}
.history-search {
flex: 1;
min-width: 200px;
position: relative;
}
.history-search input {
width: 100%;
padding: 0.6rem 1rem 0.6rem 2.5rem;
border: 1px solid #e2e8f0;
border-radius: 2rem;
font-size: 0.9rem;
background: #f8fafc;
outline: none;
transition: 0.2s;
min-height: unset;
resize: none;
}
.history-search input:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
background: #fff;
}
.history-search .search-icon {
position: absolute;
left: 0.9rem;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 0.85rem;
pointer-events: none;
}
[dir="rtl"] .history-search input {
padding: 0.6rem 2.5rem 0.6rem 1rem;
}
[dir="rtl"] .history-search .search-icon {
left: unset;
right: 0.9rem;
}
.history-sort-select {
padding: 0.6rem 1.1rem;
border-radius: 2rem;
border: 1px solid #e2e8f0;
background: #f8fafc;
font-size: 0.88rem;
font-weight: 500;
color: #1e293b;
cursor: pointer;
outline: none;
min-width: 160px;
}
.history-sort-select:focus {
border-color: #4f46e5;
}
/* ── Table ── */
.history-table-wrapper {
border-radius: 1.4rem;
overflow: hidden;
border: 1px solid #edf2f7;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.04);
}
.history-table {
width: 100%;
border-collapse: collapse;
font-size: 0.92rem;
}
.history-table thead th {
background: #f8fafc;
color: #64748b;
font-weight: 700;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.6px;
padding: 0.85rem 1.2rem;
border-bottom: 1px solid #edf2f7;
white-space: nowrap;
}
.history-table tbody tr {
border-bottom: 1px solid #f1f5f9;
transition: background 0.15s;
}
.history-table tbody tr:last-child {
border-bottom: none;
}
.history-table tbody tr:hover {
background: #f8fafc;
}
.history-table td {
padding: 0.9rem 1.2rem;
vertical-align: middle;
color: #1e293b;
}
/* Name cell */
.video-name-cell {
display: flex;
align-items: center;
gap: 0.75rem;
}
.video-thumb-icon {
width: 38px;
height: 38px;
border-radius: 0.6rem;
background: linear-gradient(135deg, #eef2ff, #e0e7ff);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.video-thumb-icon i {
color: #4f46e5;
font-size: 1rem;
}
.video-title-text {
font-weight: 600;
color: #1e293b;
word-break: break-word;
max-width: 550px;
}
.video-ext-badge {
display: inline-block;
font-size: 0.68rem;
font-weight: 700;
color: #6366f1;
background: #eef2ff;
border-radius: 0.3rem;
padding: 0.1rem 0.45rem;
margin-top: 0.18rem;
text-transform: uppercase;
}
/* Date cell */
.date-cell {
color: #475569;
font-size: 0.88rem;
white-space: nowrap;
}
.date-cell .time-sub {
display: block;
font-size: 0.78rem;
color: #94a3b8;
margin-top: 0.1rem;
}
/* Size cell */
.size-cell {
font-weight: 600;
color: #334155;
white-space: nowrap;
}
.size-cell .size-unit {
font-weight: 400;
color: #94a3b8;
font-size: 0.82rem;
}
/* Actions */
.actions-cell {
white-space: nowrap;
}
.btn-action-dl {
background: #0f172a;
color: #fff;
border: none;
padding: 0.38rem 1rem;
border-radius: 2rem;
font-size: 0.8rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.35rem;
cursor: pointer;
transition: background 0.2s, transform 0.15s;
text-decoration: none;
}
.btn-action-dl:hover {
background: #1e293b;
transform: scale(1.03);
color: #fff;
}
.btn-action-del {
background: transparent;
color: #ef4444;
border: 1px solid #fecaca;
padding: 0.38rem 0.85rem;
border-radius: 2rem;
font-size: 0.8rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.35rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-action-del:hover {
background: #fef2f2;
border-color: #ef4444;
}
/* ── Empty state ── */
.history-empty {
text-align: center;
padding: 3.5rem 1rem;
color: #94a3b8;
}
.history-empty i {
font-size: 3rem;
color: #c7d2fe;
margin-bottom: 1rem;
display: block;
}
.history-empty p {
font-size: 1rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: #64748b;
}
.history-empty span {
font-size: 0.88rem;
}
/* ── Pagination ── */
.history-pagination {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.8rem;
margin-top: 1.5rem;
}
.pagination-info {
font-size: 0.85rem;
color: #64748b;
}
/* ── Delete confirm modal ── */
.modal-confirm .modal-header {
background: #fff5f5;
border-bottom: 1px solid #fee2e2;
}
.modal-confirm .modal-title {
color: #dc2626;
font-weight: 700;
}
/* ── Responsive ── */
@media (max-width: 768px) {
.history-table thead th:nth-child(3),
.history-table tbody td:nth-child(3) {
display: none;
}
.btn-action-dl span,
.btn-action-del span {
display: none;
}
.video-title-text {
max-width: 160px;
}
}
@media (max-width: 480px) {
.history-table thead th:nth-child(2),
.history-table tbody td:nth-child(2) {
display: none;
}
}
+372
View File
@@ -0,0 +1,372 @@
$(function () {
// ------------------------- CONFIGURATION DES ENDPOINTS (à adapter avec vos vraies routes Symfony) -------------------------
// ------------------------- VARIABLES GLOBALES -------------------------
let videoEntries = []; // liste des vidéos
let entryIdCounter = 0;
let isProcessing = false; // file d'attente active ?
let isLoggedIn = false; // état connexion simulé (pour démo)
// Mise à jour affichage connexion
function updateAuthUI() {
const authDiv = $('#authToggle');
if (isLoggedIn) {
authDiv.addClass('connected');
$('#authStatusText').text('Connecté');
} else {
authDiv.removeClass('connected');
$('#authStatusText').text('Non connecté');
}
}
// Récupération des infos vidéo (nom + taille Mo)
function fetchVideoInfo(url, format) {
return new Promise((resolve, reject) => {
// Simulation d'appel AJAX vers app_frontend_size_movie
$.post(ENDPOINTS.getInfo, { url: url }, function (data) {
console.log(data);
resolve({ name: data.filename, sizeMB: data.size });
}).fail(function (response) {
reject(new Error("Erreur serveur lors de la récupération des infos"));
return;
});
});
}
// Téléchargement direct avec progression (streamed)
// Téléchargement direct via SSE avec progression réelle
function startStreamedDownload(entry, onProgress) {
return new Promise((resolve, reject) => {
const encodedVideoUrl = encodeURIComponent(entry.url);
const finalUrl = `${ENDPOINTS.streamDownload}?url=${encodedVideoUrl}&format=${entry.format}`;
const eventSource = new EventSource(finalUrl);
let isResolved = false;
// ✅ Fonction de nettoyage centralisée
function cleanup(timeoutId) {
clearTimeout(timeoutId);
eventSource.close();
}
const timeout = setTimeout(() => {
if (!isResolved) {
isResolved = true;
cleanup(null); // timeout déjà déclenché
reject(new Error('Délai d\'attente dépassé'));
}
}, 300000);
eventSource.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
if (data.progress !== undefined) {
onProgress(data.progress);
}
// ✅ Gérer l'erreur serveur
if (data.error === true) {
if (!isResolved) {
isResolved = true;
cleanup(timeout);
reject(new Error(data.message || 'Erreur serveur pendant le téléchargement'));
}
}
if (data.done === true) {
if (!isResolved) {
isResolved = true;
cleanup(timeout); // ✅ nettoyer le timeout avant resolve
updateEntry(entry.id, { pathDownload: data.pathDownload });
resolve({ success: true });
}
}
} catch (err) {
console.error('[SSE] Erreur parsing JSON:', err);
}
};
eventSource.onerror = function () {
// ✅ Ne rejeter QUE si pas encore résolu (évite faux reject après done)
if (!isResolved) {
isResolved = true;
cleanup(timeout);
reject(new Error('Erreur de connexion SSE'));
}
};
});
}
// Téléchargement en tâche de fond (barre infinie)
function startBackgroundDownload(entry) {
return new Promise((resolve, reject) => {
console.log(`[BACKGROUND] Lancement tâche de fond pour : ${entry.name}`);
// Appel POST vers app_background_download
$.post(ENDPOINTS.backgroundDownload, { url: entry.url, format: entry.format }, function (data) {
resolve({ success: true, jobId: "bg_" + entry.id });
}).fail(function (response) {
reject(new Error("Échec du téléchargement en arrière-plan"));
});
});
}
// Rendu de la liste avec les différents statuts
function renderVideoList() {
const container = $('#videoListContainer');
container.empty();
videoEntries.forEach(entry => {
let progressHtml = '';
let statusHtml = '';
let actionHtml = '';
const status = entry.status;
// Gestion de la barre de progression
if (status === 'downloading') {
const pct = entry.progress || 0;
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:${pct}%;"></div></div>`;
statusHtml = `<span class="status-text"><i class="fas fa-download"></i> Téléchargement : ${pct}%</span>`;
}
else if (status === 'background_downloading') {
// 🔄 Affiche la barre indéterminée avec la classe 'indeterminate'
progressHtml = `<div class="progress-container"><div class="progress-fill indeterminate"></div></div>`;
statusHtml = `<span class="status-text"><i class="fas fa-spinner spinner-icon"></i> Tâche de fond a été créer. une fois le traitement terminé, vous recevrez un e-mail.</span>`;
}
else if (status === 'initializing') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
statusHtml = `<span class="status-text pending-text"><i class="fas fa-spinner spinner-icon"></i> Initialisation...</span>`;
}
else if (status === 'pending') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
statusHtml = `<span class="status-text pending-text"><i class="fas fa-clock"></i> En attente</span>`;
}
else if (status === 'completed') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:100%; background:#22c55e;"></div></div>`;
statusHtml = `<span class="status-text success-text"><i class="fas fa-check-circle"></i> Terminé</span>`;
actionHtml = `<a class="action-btn download-btn" data-id="${entry.id}" href="${entry.pathDownload}"><i class="fas fa-save"></i> Télécharger</a>`;
}
else if (status === 'error') {
progressHtml = `<div class="progress-container"><div class="progress-fill error" style="width:100%;"></div></div>`;
const errMsg = entry.errorMessage || "Erreur inconnue";
statusHtml = `<span class="status-text error-text"><i class="fas fa-exclamation-triangle"></i> ${escapeHtml(errMsg)}</span>`;
}
else if (status === 'background_requires_auth') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
statusHtml = `<span class="status-text pending-text"><i class="fas fa-lock"></i> Connexion requise pour tâche de fond</span>`;
actionHtml = `<button class="action-btn outline" data-id="${entry.id}" data-action="login"><i class="fas fa-sign-in-alt"></i> Connexion</button>`;
}
else if (status === 'background_ready') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
statusHtml = `<span class="status-text pending-text"><i class="fas fa-cloud-upload-alt"></i> Prêt pour tâche de fond</span>`;
actionHtml = `<button class="btn btn-primary" data-id="${entry.id}" data-action="startBg"><i class="fas fa-play"></i> Lancer</button>`;
}
else if (status === 'ready_stream') {
// Transition rapide, normalement ne devrait pas rester visible
statusHtml = `<span class="status-text">Prêt pour téléchargement...</span>`;
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
}
const itemHtml = `
<div class="video-item" data-entry-id="${entry.id}">
<div class="video-header">
<span class="video-name"><i class="fab fa-youtube" style="color:#ef4444; margin-right:6px;"></i>${escapeHtml(entry.name)}</span>
${entry.sizeMB ? `<span style="font-size:0.7rem; background:#eef2ff; padding:0.2rem 0.6rem; border-radius:1rem;">${entry.sizeMB} Mo</span>` : ''}
</div>
${progressHtml}
<div class="status-area">
${statusHtml}
${actionHtml}
</div>
</div>
`;
container.append(itemHtml);
});
}
// Mise à jour d'une entrée spécifique et re-rendu
function updateEntry(entryId, changes) {
const index = videoEntries.findIndex(e => e.id === entryId);
if (index !== -1) {
Object.assign(videoEntries[index], changes);
renderVideoList();
}
}
// Ajout des URLs en statut "pending"
function addUrls(urlsArray, format) {
const newEntries = [];
urlsArray.forEach(url => {
const trimmed = url.trim();
if (trimmed === '') return;
const displayName = `Nouvelle vidéo (${trimmed.substring(0, 30)}...)`;
newEntries.push({
id: ++entryIdCounter,
url: trimmed,
name: displayName,
format: format,
status: 'pending',
progress: 0,
sizeMB: null,
errorMessage: ''
});
});
videoEntries.push(...newEntries);
renderVideoList();
return newEntries;
}
// Traitement d'une vidéo (séquentiel)
async function processEntry(entry) {
// Évite de traiter une vidéo déjà en cours ou terminée
if (entry.status !== 'pending' && entry.status !== 'background_ready' && entry.status !== 'background_requires_auth') {
return;
}
// 1. Initialisation : appel à getInfo
updateEntry(entry.id, { status: 'initializing', progress: 0, errorMessage: '' });
try {
const info = await fetchVideoInfo(entry.url, entry.format);
const sizeMo = info.sizeMB;
const videoName = info.name;
updateEntry(entry.id, { name: videoName, sizeMB: sizeMo });
// 2. Décision selon taille
if (sizeMo !== null && sizeMo <= 250) {
// Taille <= 250 Mo => téléchargement direct (en cours)
updateEntry(entry.id, { status: 'downloading', progress: 0 });
try {
await startStreamedDownload(entry, (prog) => {
updateEntry(entry.id, { progress: prog });
});
updateEntry(entry.id, { status: 'completed', progress: 100 });
} catch (err) {
updateEntry(entry.id, { status: 'error', errorMessage: `Échec direct : ${err.message}` });
}
}
else {
// Taille > 250 Mo ou inconnue => tâche de fond
const auth = await checkAuth();
if (auth) {
// Connecté => bouton "Lancer" (en tache de fond prêt)
updateEntry(entry.id, { status: 'background_ready' });
} else {
// Non connecté => bouton "Connexion"
updateEntry(entry.id, { status: 'background_requires_auth' });
}
}
} catch (err) {
updateEntry(entry.id, { status: 'error', errorMessage: `Erreur initialisation : ${err.message}` });
}
}
// Lancement du background après clic sur "Lancer"
async function launchBackground(entry) {
if (entry.status !== 'background_ready') return;
try {
// 🟢 Passe la vidéo en mode 'background_downloading' pour déclencher l'affichage de la barre indéterminée
updateEntry(entry.id, { status: 'background_downloading', progress: 0 });
await startBackgroundDownload(entry);
} catch (err) {
// 🔴 En cas d'erreur, affiche le message d'erreur
updateEntry(entry.id, { status: 'error', errorMessage: `Tâche de fond échouée : ${err.message}` });
}
}
// Gestion de la file d'attente séquentielle
async function processQueue() {
if (isProcessing) return;
isProcessing = true;
// On traite uniquement les vidéos en attente ou celles en background_ready/requires_auth ?
// Pour éviter de bloquer la queue, on ne traite que les "pending"
const pendingEntries = videoEntries.filter(e => e.status === 'pending');
for (const entry of pendingEntries) {
await processEntry(entry);
// Une fois cette vidéo traitée (même si elle est en background_ready ou error), on passe à la suivante
// Mais attention : si la vidéo est en background_ready, elle attend une action utilisateur, donc on ne bloque pas la file.
}
isProcessing = false;
}
// Déclenche l'ajout des URLs et démarre la queue
function startNewDownloads(urls, format) {
if (urls.length === 0) {
alert("Ajoutez au moins une URL YouTube.");
return false;
}
const newEntries = addUrls(urls, format);
processQueue();
return true;
}
// Gestion des clics sur les boutons (connexion, lancer background, téléchargement final)
$(document).on('click', '.action-btn', async function (e) {
const entryId = $(this).data('id');
const action = $(this).data('action');
const entry = videoEntries.find(e => e.id === entryId);
if (!entry) return;
if (action === 'login') {
window.open(ENDPOINTS.urlAuth, "_self");
}
else if (action === 'startBg') {
if (entry.status === 'background_ready') {
await launchBackground(entry);
// Après fin du background, la queue pourrait reprendre s'il y a d'autres pending,
// mais on ne relance pas automatiquement processQueue pour ne pas interférer.
}
}
/*else if ($(this).hasClass('download-btn') && entry.status === 'completed') {
// Téléchargement final du fichier (simulé)
//window.open(ENDPOINTS.downloadFileAnonyme, "_self");
console.log();
//alert(`Téléchargement du fichier : ${entry.name}\nFormat : ${entry.format}\n(Simulation - fichier prêt)`);
}*/
});
// Bascule manuelle de connexion (pour démo)
$('#authToggle').on('click', function () {
isLoggedIn = !isLoggedIn;
updateAuthUI();
// Pour chaque vidéo en background_requires_auth, on la repasse en background_ready
videoEntries.forEach(entry => {
if (entry.status === 'background_requires_auth' && isLoggedIn) {
updateEntry(entry.id, { status: 'background_ready' });
} else if (entry.status === 'background_ready' && !isLoggedIn) {
updateEntry(entry.id, { status: 'background_requires_auth' });
}
});
renderVideoList();
});
// Bouton principal "Démarrer le traitement"
$('#processBtn').on('click', function () {
const urlText = $('#youtubeUrls').val();
const urls = urlText.split('\n').filter(line => line.trim() !== '');
const format = $('#formatSelect').val();
if (urls.length === 0) {
alert("Collez au moins une URL YouTube.");
return;
}
startNewDownloads(urls, format);
$('#youtubeUrls').val(''); // nettoyer
});
// Utilitaires
function escapeHtml(text) {
return String(text).replace(/[&<>]/g, function (m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
// Initialisation
updateAuthUI();
renderVideoList();
});
+51
View File
@@ -0,0 +1,51 @@
$(function () {
/* ── Delete modal: inject video id & name ── */
$('#deleteModal').on('show.bs.modal', function (e) {
const btn = $(e.relatedTarget);
const videoId = btn.data('video-id');
const name = btn.data('video-name');
const url = '/delete-video/' + videoId; // Adapt to your route
$('#deleteModalFileName').text(name);
$('#deleteForm').attr('action', url);
});
/* ── Live search ── */
$('#searchInput').on('input', function () {
const query = $(this).val().toLowerCase().trim();
let visible = 0;
$('#historyTbody tr').each(function () {
const name = $(this).data('name') || '';
const match = name.includes(query);
$(this).toggle(match);
if (match) visible++;
});
updatePaginationInfo(visible);
});
/* ── Sort ── */
$('#sortSelect').on('change', function () {
const val = $(this).val();
const tbody = $('#historyTbody');
const rows = tbody.find('tr').get();
rows.sort(function (a, b) {
switch (val) {
case 'date_desc': return $(b).data('date') - $(a).data('date');
case 'date_asc': return $(a).data('date') - $(b).data('date');
case 'name_asc': return ($(a).data('name') > $(b).data('name')) ? 1 : -1;
case 'size_desc': return $(b).data('size') - $(a).data('size');
default: return 0;
}
});
tbody.empty();
$.each(rows, function (i, row) { tbody.append(row); });
});
function updatePaginationInfo(count) {
const total = $('#historyTbody tr').length;
// Simple update — replace the number shown; adapt translation if needed
$('#paginationInfo').text(count + ' / ' + total);
}
});
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,601 @@
/*!
* Bootstrap Reboot v5.3.8 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
line-height: inherit;
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type=search]::-webkit-search-cancel-button {
cursor: pointer;
filter: grayscale(1);
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,598 @@
/*!
* Bootstrap Reboot v5.3.8 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
line-height: inherit;
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type=search]::-webkit-search-cancel-button {
cursor: pointer;
filter: grayscale(1);
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
node_modules/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Mark French
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+205
View File
@@ -0,0 +1,205 @@
# Country Select JS
A jQuery plugin for selecting a country, based on the excellent [International Telephone Input](https://github.com/Bluefieldscom/intl-tel-input.git) plugin. It adds a flag dropdown to any input, which lists all the countries in English and the predominant national language next to their flags.
![alt tag](https://raw.github.com/mrmarkfrench/country-select-js/master/screenshot.png)
## Table of Contents
- [Demo](#demo)
- [Features](#features)
- [Getting Started](#getting-started)
- [Options](#options)
- [Public Methods](#public-methods)
- [Static Methods](#static-methods)
- [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
- [Attributions](#attributions)
## Demo
Try it for yourself using the included demo.html.
## Features
* Automatically select the country as the user types
* Navigate the country dropdown by typing a country's name, or using up/down keys
* Selecting a country from the dropdown will update the country name in the input
* Dropdown appears above or below the input depending on available space/scroll position
* Lots of initialisation options for customisation, as well as public methods for interaction
* Can optionally update a related field with the two-letter ISO country code on selection
## Getting Started
1. Download the [latest version](https://github.com/mrmarkfrench/country-select-js/archive/master.zip)
2. Link the stylesheet (note that this references the image flags.png)
```html
<link rel="stylesheet" href="build/css/countrySelect.css">
```
3. Add the plugin script and initialise it on your input element
```html
<input type="text" id="country">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="build/js/countrySelect.min.js"></script>
<script>
$("#country").countrySelect();
</script>
```
4. Optional: add an extra input field (with type hidden or text) named the same as your selector input appended with "_code". This will automatically be updated with the [ISO 3166-1 alpha-2](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) code for the selected country.
```html
<input type="text" id="country" />
<input type="hidden" id="country_code" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="build/js/countrySelect.min.js"></script>
<script>
$("#country").countrySelect();
</script>
```
## Options
Pass an (optional) object as a parameter to the `countrySelect` method when initializing the selector.
```
$("#country").countrySelect({
defaultCountry: "jp",
onlyCountries: ['us', 'gb', 'ch', 'ca', 'do', 'jp'],
preferredCountries: ['ca', 'gb', 'us'],
responsiveDropdown: true
});
```
Note: any options that take country codes should be lower case [ISO 3166-1 alpha-2](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) codes
**defaultCountry**
Type: `String` Default: `""`
Set the default country by it's country code. Otherwise it will just be the first country in the list.
**onlyCountries**
Type: `Array` Default: `undefined`
Display only the countries you specify. Takes an array of country codes.
**excludeCountries**
Type: `Array` Default: `undefined`
Display only the countries not specified. Takes an array of country codes.
**preferredCountries**
Type: `Array` Default: `["us", "gb"]`
Specify the countries to appear at the top of the list.
**responsiveDropdown**
Type: `Boolean` Default: `false`
Set the dropdown's width to be the same as the input. This is automatically enabled for small screens.
**localizedCountries**
Type: `Object` Default: `{}`
Allows to translate the countries by its given iso code e.g.:
```js
{ 'de': 'Deutschland' }
```
## Public Methods
**destroy**
Remove the plugin from the input, and unbind any event listeners.
```js
$("#country").countrySelect("destroy");
```
**getSelectedCountryData**
Get the country data for the currently selected flag.
```js
var countryData = $("#country").countrySelect("getSelectedCountryData");
```
Returns something like this:
```js
{
name: "Afghanistan (‫افغانستان‬)",
iso2: "af",
}
```
**selectCountry**
Change the country selection (e.g. when the user is entering their address).
```js
$("#country").countrySelect("selectCountry", "gb");
```
**setCountry**
Insert a country name, and update the selected flag accordingly.
```js
$("#country").countrySelect("setCountry", "United States");
```
## Static Methods
**getCountryData**
Get all of the plugin's country data.
```js
var countryData = $.fn.countrySelect.getCountryData();
```
Returns an array of country objects:
```js
[{
name: "Afghanistan (‫افغانستان‬)",
iso2: "af",
}, ...]
```
**setCountryData**
Set all of the plugin's country data.
```js
$.fn.countrySelect.setCountryData(countryData);
```
## Troubleshooting
**Image path**
Depending on your project setup, you may need to override the path to flags.png in your CSS.
```css
.country-select .flag {background-image: url("path/to/flags.png");}
```
**Full width input**
If you want your input to be full-width, you need to set the container to be the same i.e.
```css
.country-select {width: 100%;}
```
**Input margin**
For the sake of alignment, the default CSS forces the input's vertical margin to `0px`. If you want vertical margin, you should add it to the container (with class `country-select`).
**Displaying error messages**
If your error handling code inserts an error message before the `<input>` it will break the layout. Instead you must insert it before the container (with class `country-select`).
**Dropdown position**
The dropdown should automatically appear above/below the input depending on the available space. For this to work properly, you must only initialise the plugin after the `<input>` has been added to the DOM.
## Contributing
Contributions and improvements to the library are welcome! For instructions on contributing to a project on Github, see this guide: [Fork A Repo](https://help.github.com/articles/fork-a-repo).
If you are treating the library as a Node package, the following will be relevant to you.
> To start a local devserver with source code live reload install the dependencies with:
>
> ```
> $ npm install
> ```
>
> And run:
>
> ```
> $ gulp
> ```
>
> To transpile the scss source, minify and prepare your changes at src to build run:
>
>```
> $ gulp build
> ```
## Attributions
* This library is built based on the excellent [International Telephone Input](https://github.com/Bluefieldscom/intl-tel-input.git), the authors of which deserve any credit you might like to give (though none of the blame)
* Flag images and CSS from [Region-Flags](https://github.com/behdad/region-flags)
* Original country data from mledoze's [World countries in JSON, CSV and XML](https://github.com/mledoze/countries)
+34
View File
@@ -0,0 +1,34 @@
{
"name": "country-select-js",
"version": "2.1.1",
"description": "A jQuery plugin for selecting a country",
"main": [
"build/js/countrySelect.js",
"build/css/countrySelect.css"
],
"dependencies": {
"jquery": ">=1.9.1"
},
"moduleType": [
"amd",
"globals",
"node"
],
"keywords": [
"country",
"international",
"jQuery"
],
"authors": [
"Mark French"
],
"license": "MIT",
"ignore": [
"**/.*",
"demo.html"
],
"repository": {
"type": "git",
"url": "git://github.com/mrmarkfrench/country-select-js.git"
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
* {
box-sizing: border-box;
-moz-box-sizing: border-box; }
body {
margin: 20px;
font-size: 14px;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
color: #555; }
.hide {
display: none; }
pre {
margin: 0 !important;
display: inline-block; }
.form-item {
margin: 1em;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
background: none; }
input, button {
height: 35px;
margin: 0;
padding: 6px 12px;
border-radius: 2px;
font-family: inherit;
font-size: 100%;
color: inherit; }
input[disabled], button[disabled] {
background-color: #eee; }
input, select {
border: 1px solid #CCC;
width: 250px; }
::-webkit-input-placeholder {
color: #BBB; }
::-moz-placeholder {
/* Firefox 19+ */
color: #BBB;
opacity: 1; }
:-ms-input-placeholder {
color: #BBB; }
button {
color: #FFF;
background-color: #428BCA;
border: 1px solid #357EBD; }
button:hover {
background-color: #3276B1;
border-color: #285E8E;
cursor: pointer; }
#result {
margin-bottom: 100px; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Country Select JS</title>
<link rel="stylesheet" href="build/css/countrySelect.css">
<link rel="stylesheet" href="build/css/demo.css">
</head>
<body>
<h1>Country Select JS</h1>
<form>
<div class="form-item">
<input id="country_selector" type="text">
<label for="country_selector" style="display:none;">Select a country here...</label>
</div>
<div class="form-item" style="display:none;">
<input type="text" id="country_selector_code" name="country_selector_code" data-countrycodeinput="1" readonly="readonly" placeholder="Selected country code will appear here" />
<label for="country_selector_code">...and the selected country code will be updated here</label>
</div>
<button type="submit" style="display:none;">Submit</button>
</form>
<!-- Load jQuery from CDN so can run demo immediately -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="build/js/countrySelect.js"></script>
<script>
$("#country_selector").countrySelect({
// defaultCountry: "jp",
// onlyCountries: ['us', 'gb', 'ch', 'ca', 'do'],
// responsiveDropdown: true,
preferredCountries: ['ca', 'gb', 'us']
});
</script>
</body>
</html>
@@ -0,0 +1,72 @@
var gulp = require('gulp');
var sass = require('gulp-sass');
var prefix = require('gulp-autoprefixer');
var notify = require('gulp-notify');
var cleanCSS = require('gulp-clean-css');
var minifyJS = require('gulp-minify');
var rename = require('gulp-rename');
var webserver = require('gulp-webserver');
var runSequence = require('run-sequence');
gulp.task('scss', function () {
return gulp.src('./src/scss/countrySelect.scss')
.pipe(sass({ errLogToConsole: true }))
.pipe(prefix())
.pipe(cleanCSS({compatibility: 'ie8', format: {
breaks: {
afterAtRule: true,
afterBlockBegins: true,
afterBlockEnds: true,
afterComment: true,
afterProperty: true,
afterRuleBegins: true,
afterRuleEnds: true,
beforeBlockEnds: true,
betweenSelectors: true
},
indentBy: 1,
indentWith: 'tab' }, level: 0}))
.pipe(gulp.dest('./build/css'))
.pipe(notify("styles compiled"));
});
gulp.task('js', function () {
return gulp.src('./src/js/countrySelect.js')
.pipe(gulp.dest('./build/js'))
.pipe(notify("javascript updated"));
});
gulp.task('handle-sources', ['scss', 'js']);
gulp.task('minify-scss', function () {
return gulp.src('./build/css/countrySelect.css')
.pipe(cleanCSS({level: 2, inline: ['all']}))
.pipe(rename({extname: '.min.css'}))
.pipe(gulp.dest('./build/css'))
.pipe(notify("styles minified"));
});
gulp.task('minify-js', function () {
return gulp.src('./build/js/countrySelect.js')
.pipe(minifyJS({ext:{min:'.min.js'}}))
.pipe(gulp.dest('./build/js'))
.pipe(notify("javascript minified"));
});
gulp.task('minify-sources', ['minify-scss', 'minify-js']);
gulp.task('webserver', function() {
gulp.src('.')
.pipe(webserver({
livereload: true,
directoryListing: true,
open: './demo.html'
}));
gulp.watch('./src/scss/**/*.scss', ['scss']);
gulp.watch('./src/js/**/*.js', ['js']);
});
gulp.task('default', ['handle-sources', 'webserver']);
gulp.task('build', function() {
runSequence('handle-sources', 'minify-sources');
});
+1
View File
@@ -0,0 +1 @@
module.exports = require('./build/js/countrySelect.js');
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
{
"name": "country-select-js",
"version": "2.1.1",
"description": "A jQuery plugin for selecting a country",
"main": "build/js/countrySelect.js",
"repository": {
"type": "git",
"url": "https://mrmarkfrench@github.com/mrmarkfrench/country-select-js.git"
},
"keywords": [
"country",
"international",
"jQuery"
],
"scripts": {
"build": "gulp build"
},
"author": "Mark French",
"license": "MIT",
"bugs": {
"url": "https://github.com/mrmarkfrench/country-select-js/issues"
},
"homepage": "https://github.com/mrmarkfrench/country-select-js",
"devDependencies": {
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.0.0",
"gulp-clean-css": "^3.3.1",
"gulp-minify": "^1.0.0",
"gulp-notify": "^3.0.0",
"gulp-rename": "^1.2.2",
"gulp-sass": "^3.1.0",
"gulp-webserver": "^0.9.1",
"run-sequence": "^2.2.1"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,214 @@
.country-select {
// need position on the container so the selected flag can be
// absolutely positioned over the input
position: relative;
// keep the input's default inline properties
display: inline-block;
// paul irish says this is ok
// http://www.paulirish.com/2012/box-sizing-border-box-ftw/
* {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.hide {
display: none;
}
// need this during init, to get the height of the dropdown
.v-hide {
visibility: hidden;
}
// specify types to increase specificity e.g. to override bootstrap v2.3
input, input[type=text] {
position: relative;
// input is bottom level, below selected flag and dropdown
z-index: 0;
// any vertical margin the user has on their inputs would no longer work as expected
// because we wrap everything in a container div. i justify the use of !important
// here because i don't think the user should ever have vertical margin here - when
// the input is wrapped in a container, vertical margin messes up alignment with other
// inline elements (e.g. an adjacent button) in firefox, and probably other browsers.
margin-top: 0 !important;
margin-bottom: 0 !important;
// make space for the selected flag
// Note: no !important here, as the user may want to tweak this so that the
// perceived input padding matches their existing styles
padding-right: $selected-flag-width;
// any margin-right here will push the selected-flag away
margin-right: 0;
}
.flag-dropdown {
// positioned over the top of the input
position: absolute;
// full height
top: 0;
bottom: 0;
right: 0;
// prevent the highlighted child from overlapping the input border
padding: $border-width;
}
.selected-flag {
// render above the input
z-index: 1;
position: relative;
width: $selected-flag-width;
// this must be full-height both for the hover highlight, and to push down the
// dropdown so it appears below the input
height: 100%;
padding: 0 0 0 $flag-padding;
// vertically center the flag
.flag {
position: absolute;
top: 0;
bottom: 0;
margin: auto;
}
.arrow {
position: absolute;
// split the difference between the flag and the arrow height to verically center
top: 50%;
margin-top: -1 * ($arrow-height / 2);
right: $arrow-padding;
// css triangle
width: 0;
height: 0;
border-left: $triangle-border solid transparent;
border-right: $triangle-border solid transparent;
border-top: $arrow-height solid $arrow-color;
&.up {
border-top: none;
border-bottom: $arrow-height solid $arrow-color;
}
}
}
// the dropdown
.country-list {
position: absolute;
// popup so render above everything else
z-index: 2;
// override default list styles
list-style: none;
// in case any container has text-align:center
text-align: left;
// dropdown flags need consistent width, so wrap in a container
.flag {
display: inline-block;
width: $flag-width;
}
padding: 0;
// margin-left to compensate for the padding on the parent
margin: 0 0 0 (-$border-width);
box-shadow: 1px 1px 4px rgba(0,0,0,0.2);
background-color: white;
border: $border-width solid $grey-border;
// don't let the contents wrap AKA the container will be as wide as the contents
white-space: nowrap;
// except on small screens, where we force the dropdown width to match the input
@media (max-width: 500px) {
white-space: normal;
}
max-height: 200px;
overflow-y: scroll;
// the divider below the preferred countries
.divider {
padding-bottom: 5px;
margin-bottom: 5px;
border-bottom: $border-width solid $grey-border;
}
// each country item in dropdown (we must have separate class to differentiate from dividers)
.country {
// Note: decided not to use line-height here for alignment because it causes issues e.g. large font-sizes will overlap, and also looks bad if one country overflows onto 2 lines
padding: 5px 10px;
}
.country.highlight {
background-color: $hover-color;
}
.flag, .country-name {
vertical-align: middle;
}
// spacing between country flag and name
.flag {
margin-right: 6px;
}
}
&.inside {
input, input[type=text] {
padding-right: $input-padding;
padding-left: $selected-flag-arrow-width + $input-padding;
margin-left: 0;
}
.flag-dropdown {
right: auto;
left: 0;
}
.selected-flag {
width: $selected-flag-arrow-width;
}
}
&.inside {
// hover state - show flag is clickable
.flag-dropdown:hover {
cursor: pointer;
.selected-flag {
background-color: $hover-color;
}
}
// disable hover state when input is disabled
input[disabled] + .flag-dropdown:hover, input[readonly] + .flag-dropdown:hover {
cursor: default;
.selected-flag {
background-color: transparent;
}
}
}
.flag {
width: $flag-width;
height: $flag-height;
box-shadow: 0px 0px 1px 0px #888;
background-image: url("#{$flags-image-path}#{$flags-image-name}.#{$flags-image-extension}");
background-repeat: no-repeat;
// empty state
background-color: #dbdbdb;
background-position: $flag-width 0;
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2 / 1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
background-image: url("#{$flags-image-path}#{$flags-image-name}@2x.#{$flags-image-extension}");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
@function retina-size($value) {
@return floor($value / 2);
}
@mixin retina-bg-size($spriteWidth, $spriteHeight) {
background-size: floor($spriteWidth / 2) floor($spriteHeight / 2);
}
@@ -0,0 +1,30 @@
// rgba is needed for the selected flag hover state to blend in with
// the border-highlighting some browsers give the input on focus
$hover-color: rgba(0, 0, 0, 0.05) !default;
$grey-text: #999 !default;
$grey-border: #ccc !default;
$flag-height: 15px !default;
$flag-width: 20px !default;
$flag-padding: 8px !default;
// this border width is used for the popup and divider, but it is also
// assumed to be the border width of the input, which we do not control
$border-width: 1px !default;
$arrow-height: 4px !default;
$arrow-width: 6px !default;
$triangle-border: 3px !default;
$arrow-padding: 6px !default;
$arrow-color: #555 !default;
$input-padding: 6px !default;
$selected-flag-width: $flag-width + (2 * $flag-padding) !default;
$selected-flag-arrow-width: $flag-width + $flag-padding + $arrow-width + (2 * $arrow-padding) !default;
// image related variables
$flags-image-path: "../../build/img/" !default;
$flags-image-name: "flags" !default;
$flags-image-extension: "png" !default;
// enough space for them to click off to close
$mobile-popup-margin: 30px;

Some files were not shown because too many files have changed in this diff Show More