diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cd224cf --- /dev/null +++ b/.env.example @@ -0,0 +1,61 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Configuracao de producao — https://gustavoo.me +# +# Uso na VPS: +# cp .env.example .env +# # gere senhas novas: openssl rand -base64 24 +# docker compose up -d +# +# O proxy reverso / HTTPS da VPS deve encaminhar para a porta abaixo. +# O WordPress responde em HTTP e o proxy cuida do certificado. +# ───────────────────────────────────────────────────────────────────────────── + +# Porta HTTP do WordPress consumida pelo proxy reverso. +WORDPRESS_PORT=8080 + +# ── Site ───────────────────────────────────────────────────────────────────── +WORDPRESS_SITE_URL=https://gustavoo.me +WORDPRESS_SITE_TITLE=Gustavo Desenvolvedor Full Stack | Web, Mobile, Infraestrutura & Automação + +# ── Banco de dados (MariaDB) ───────────────────────────────────────────────── +WORDPRESS_DB_NAME=wordpress +WORDPRESS_DB_USER=wordpress +WORDPRESS_DB_PASSWORD=TROQUE_POR_UMA_SENHA_FORTE +MARIADB_ROOT_PASSWORD=TROQUE_POR_UMA_SENHA_ROOT_FORTE + +# ── Administrador criado no primeiro boot ──────────────────────────────────── +# Troque a senha depois de instalar. Em uma instalacao existente, nada aqui +# altera usuarios ja criados. +WORDPRESS_ADMIN_USER=admin +WORDPRESS_ADMIN_PASSWORD=TROQUE_POR_UMA_SENHA_FORTE +WORDPRESS_ADMIN_EMAIL=contato@gustavoo.me + +# ── Idioma ─────────────────────────────────────────────────────────────────── +WP_LOCALE=pt_BR + +# ── Plugins / tema ─────────────────────────────────────────────────────────── +# Akismet e Hello Dolly (padroes do WordPress) sao removidos. +# Query Monitor e ferramenta de desenvolvimento: mantenha fora de producao. +FORBIDDEN_PATTERN=^akismet$|^hello$|^query-monitor$ +# WP_ALLOW_FORBIDDEN_PLUGINS=1 # desativa qualquer remocao +WP_THEME=gustavoo-portfolio +# WP_KEEP_THEMES= # temas extras preservados (separados por espaco) + +# ── Plugins instalados automaticamente do WordPress.org ────────────────────── +# Instalados apenas se faltarem e sempre ativados. Ordem: slug do diretorio ou URL. +# post-views-counter -> contador de views (posts + paginas, posicao manual) +# seo-by-rank-math -> Rank Math SEO (sem configuracao, wizard manual) +# google-site-kit -> Site Kit por Google (sem configuracao, conexao manual) +WP_EXTRA_PLUGINS=post-views-counter seo-by-rank-math google-site-kit + +# ── Redis object cache ─────────────────────────────────────────────────────── +WP_REDIS_HOST=redis +WP_REDIS_PORT=6379 +# WP_REDIS_PASSWORD= +# WP_REDIS_DATABASE=0 + +# ── Atualizacoes automaticas ao reiniciar (modo update) ────────────────────── +# Em producao, atualizar sem revisao pode quebrar o tema. Deixe desativado e +# atualize de proposito: WP_SKIP_UPDATES= (vazio = atualiza) +# Para nunca atualizar automaticamente, descomente: +# WP_SKIP_UPDATES=1 diff --git a/.gitignore b/.gitignore index 905be28..b048435 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,14 @@ wp-content/plugins/query-monitor/ wp-content/plugins/hello.php wp-content/plugins/index.php +# Third-party plugins installed from WordPress.org by docker/wp-init.sh +# (WP_EXTRA_PLUGINS). They are downloaded on first run and updated in place, so +# they are not versioned here: committing them would add ~45 MB of code that the +# setup can fetch on demand. +wp-content/plugins/post-views-counter/ +wp-content/plugins/seo-by-rank-math/ +wp-content/plugins/google-site-kit/ + # Redis object cache drop-in (auto-installed by wpcli service) wp-content/object-cache.php diff --git a/README.md b/README.md index 10d5736..23bff32 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,16 @@ O tema e plugin dependem dos seguintes plugins WordPress (instalados e ativados) - **FluentCRM** — Automação de marketing e newsletter ```bash -# Subir ambiente Docker +# Criar o arquivo de variáveis de ambiente (obrigatório) +cp .env.example .env +# Edite o .env com suas credenciais (senhas do banco, porta, etc.) + +# Subir ambiente Docker (instala e configura tudo automaticamente) docker compose up -d +# Acompanhar a instalação +docker compose logs -f wpcli + # Acessar WordPress # http://localhost:8080 # Usuário: admin | Senha: admin (definido no .env) @@ -102,6 +109,49 @@ docker compose up -d docker compose logs -f wordpress ``` +### O que acontece a cada `docker compose up` + +O serviço `wpcli` roda o `docker/wp-init.sh` e detecta o estado do banco: + +**Primeira execução (banco vazio) — instalação** +1. `wp core install` com URL/título/admin definidos no `.env` +2. Pacote de idioma `pt_BR` +3. Instala do WordPress.org os plugins de `WP_EXTRA_PLUGINS` (Post Views Counter, Rank Math SEO, Site Kit) — só se faltarem +4. Ativa **todos** os plugins de `wp-content/plugins/` e remove os que o WordPress traz (`akismet`, `hello`) +5. Redis: constantes no `wp-config.php`, plugin, drop-in `object-cache.php` e verificação de conexão +6. Configura o Post Views Counter: conta **posts e páginas**, posição **Manual only** +7. Ativa o tema `gustavoo-portfolio` e remove os temas padrão +8. Executa o seeder do plugin (páginas, formulários, feeds, permalinks, avatar) + +Rank Math SEO e Site Kit são apenas instalados e ativados: o wizard e a conexão com o Google são feitos manualmente no painel. + +**Execuções seguintes (site já instalado) — atualização** +1. Nada é reinstalado e nenhum dado é apagado +2. Atualiza o **core** e os **plugins ativos** que tiverem versão nova +3. Ativa plugins novos adicionados à pasta `wp-content/plugins/` +4. Reaplica só o que estiver faltando (idioma, Redis, tema, seeder idempotente) + +> A configuração do Post Views Counter roda **uma única vez**, na instalação. Se você mudar a posição no painel, ela não é sobrescrita nos próximos boots. + +O seeder também aplica, apenas enquanto os valores ainda forem os de instalação nova: + +| Ajuste | Valor aplicado | +|---|---| +| Links permanentes | **Dia e nome** — `/ano/mês/dia/slug/` | +| Avatar padrão | **Iniciais** | + +Depois de criar permalinks, o `.htaccess` do Apache é regravado na hora — sem isso as URLs novas dariam 404. Se você já tiver escolhido outro permalink ou avatar, sua escolha é mantida. + +Tudo o que é do usuário fica em **volumes**, não no container: + +| Volume | Conteúdo | +|---|---| +| `db_data` | banco MariaDB — posts, páginas, usuários, opções, formulários | +| `wordpress_data` | core, uploads, plugins e temas instalados | +| `redis_data` | cache de objetos | + +`docker compose down` (sem `-v`) preserva tudo; só `down -v` recria do zero. Para pular atualizações numa reinicialização, use `WP_SKIP_UPDATES=1` no `.env`. + ## Deploy O tema está preparado para deploy em ambientes WordPress padrão (VPS, shared hosting, WP Engine, Kinsta, etc.). diff --git a/compose.yaml b/compose.yaml index a519067..4d0ce18 100644 --- a/compose.yaml +++ b/compose.yaml @@ -49,7 +49,7 @@ services: wpcli: image: wordpress:cli-php8.3 - user: "33:33" + user: "${WPCLI_UID:-33}:${WPCLI_GID:-33}" depends_on: wordpress: condition: service_started @@ -60,6 +60,23 @@ services: WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME} WORDPRESS_DB_USER: ${WORDPRESS_DB_USER} WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD} + WORDPRESS_PORT: ${WORDPRESS_PORT:-8080} + WORDPRESS_SITE_URL: ${WORDPRESS_SITE_URL:-http://localhost:${WORDPRESS_PORT:-8080}} + WORDPRESS_SITE_TITLE: ${WORDPRESS_SITE_TITLE:-Gustavo Portfolio} + WORDPRESS_ADMIN_USER: ${WORDPRESS_ADMIN_USER:-admin} + WORDPRESS_ADMIN_PASSWORD: ${WORDPRESS_ADMIN_PASSWORD:-admin} + WORDPRESS_ADMIN_EMAIL: ${WORDPRESS_ADMIN_EMAIL:-admin@example.com} + WP_REDIS_HOST: ${WP_REDIS_HOST:-redis} + WP_REDIS_PORT: ${WP_REDIS_PORT:-6379} + WP_REDIS_PASSWORD: ${WP_REDIS_PASSWORD:-} + WP_REDIS_DATABASE: ${WP_REDIS_DATABASE:-} + WP_EXTRA_PLUGINS: ${WP_EXTRA_PLUGINS:-post-views-counter seo-by-rank-math google-site-kit} + WP_LOCALE: ${WP_LOCALE:-pt_BR} + WP_SKIP_UPDATES: ${WP_SKIP_UPDATES:-} + WP_THEME: ${WP_THEME:-gustavoo-portfolio} + WP_KEEP_THEMES: ${WP_KEEP_THEMES:-} + FORBIDDEN_PATTERN: ${FORBIDDEN_PATTERN:-} + WP_ALLOW_FORBIDDEN_PLUGINS: ${WP_ALLOW_FORBIDDEN_PLUGINS:-} HOME: /tmp volumes: - wordpress_data:/var/www/html diff --git a/docker/wp-init.sh b/docker/wp-init.sh old mode 100644 new mode 100755 index 6729abb..18d3c4f --- a/docker/wp-init.sh +++ b/docker/wp-init.sh @@ -1,49 +1,379 @@ #!/bin/sh set -e -echo "[wpcli] Waiting for WordPress core install..." -until wp core is-installed --path=/var/www/html 2>/dev/null; do - sleep 3 +# Two modes: +# install — the database has no WordPress yet: core, language, plugins, +# theme and the project seeder are set up from scratch. +# update — the site already exists: only the WordPress core and the +# installed plugins are updated. Content, options, widgets, +# users and manually configured settings are never touched. +# +# Whatever a plugin/theme adds on disk (uploads, new plugin folders, edited +# files) survives, because state lives in the db_data and wordpress_data +# volumes, not in this script. + +WP_PATH=/var/www/html +SITE_URL="${WORDPRESS_SITE_URL:-http://localhost:${WORDPRESS_PORT:-8080}}" +ADMIN_USER="${WORDPRESS_ADMIN_USER:-admin}" +ADMIN_PASSWORD="${WORDPRESS_ADMIN_PASSWORD:-admin}" +ADMIN_EMAIL="${WORDPRESS_ADMIN_EMAIL:-admin@example.com}" +ADMIN_TITLE="${WORDPRESS_SITE_TITLE:-Gustavo Portfolio}" +WP_THEME="${WP_THEME:-gustavoo-portfolio}" +WP_LOCALE="${WP_LOCALE:-pt_BR}" +# Plugins from the WordPress.org directory that must exist even when they are +# not committed to wp-content/plugins. Installed only when missing, then +# activated. Format: slug (or URL). +WP_EXTRA_PLUGINS="${WP_EXTRA_PLUGINS:-post-views-counter seo-by-rank-math google-site-kit}" + +wp_run() { + wp --path="$WP_PATH" "$@" +} + +log() { + echo "[wpcli] $*" +} + +warn() { + echo "[wpcli] WARN: $*" >&2 +} + +# ─── Wait for dependencies ─────────────────────────────────────────────────── +log "Waiting for database..." +until wp_run db check >/dev/null 2>&1; do + sleep 2 done -if ! wp language core is-installed pt_BR --path=/var/www/html 2>/dev/null; then - echo "[wpcli] Installing pt_BR language pack..." - wp language core install pt_BR --path=/var/www/html +log "Waiting for WordPress core files..." +until [ -f "$WP_PATH/wp-load.php" ]; do + sleep 2 +done + +# The wp:cli image runs as a different uid than the wordpress image. Grant +# write access to whoever needs it without changing the volume ownership: +# the core folder (plugins, themes, wp-config.php) and wp-content itself. +# Bind-mounted wp-content/plugins|themes come from the host, so their +# permissions are checked too. +if [ ! -w "$WP_PATH/wp-content" ]; then + log "$WP_PATH/wp-content not writable by uid $(id -u), fixing permissions..." + find "$WP_PATH" -maxdepth 1 -exec chmod g+rwX {} + || true + find "$WP_PATH/wp-content" -maxdepth 1 -exec chmod g+rwX {} + || true + mkdir -p "$WP_PATH/wp-content/uploads" 2>/dev/null || true + find "$WP_PATH/wp-content/uploads" -maxdepth 1 -exec chmod g+rwX {} + 2>/dev/null || true fi -echo "[wpcli] Activating pt_BR site language..." -wp language core activate pt_BR --path=/var/www/html +# Plugins are installed by wp-cli inside the bind-mounted folder, so wp-cli +# must be able to write there. The compose default (user 33:33) matches the +# wordpress image; a host-owned folder is reported instead of failing silently. +for dir in "$WP_PATH/wp-content/plugins" "$WP_PATH/wp-content/themes"; do + [ -d "$dir" ] || continue + if [ ! -w "$dir" ]; then + log "$dir is owned by another user — adjusting group permissions..." + find "$dir" -maxdepth 0 -exec chmod g+rwX {} + 2>/dev/null || true + fi + if [ ! -w "$dir" ]; then + warn "cannot write to $dir as uid $(id -u)/gid $(id -g). Plugin install/update will fail." + warn "fix with: sudo chmod -R g+rwX wp-content/plugins wp-content/themes && sudo chgrp $(id -gn) wp-content/plugins wp-content/themes" + fi +done -echo "[wpcli] Setting Redis connection constants..." -wp config set WP_REDIS_HOST redis --type=constant --path=/var/www/html -wp config set WP_REDIS_PORT 6379 --type=constant --raw --path=/var/www/html - -if ! wp plugin is-installed redis-cache --path=/var/www/html; then - echo "[wpcli] Installing Redis Object Cache plugin..." - wp plugin install redis-cache --path=/var/www/html +if [ ! -w "$WP_PATH" ]; then + echo "[wpcli] ERROR: $WP_PATH is not writable by uid $(id -u). Aborting." >&2 + exit 1 fi -echo "[wpcli] Activating Redis Object Cache plugin..." -wp plugin activate redis-cache --path=/var/www/html - -echo "[wpcli] Enabling Redis object cache..." -wp redis enable --path=/var/www/html || wp redis update-dropin --path=/var/www/html - -echo "[wpcli] Redis setup complete." - -if ! wp plugin is-installed query-monitor --path=/var/www/html; then - echo "[wpcli] Installing Query Monitor plugin..." - wp plugin install query-monitor --path=/var/www/html +# ─── Mode detection ────────────────────────────────────────────────────────── +if wp_run core is-installed 2>/dev/null; then + MODE=update +else + MODE=install fi -echo "[wpcli] Activating Query Monitor plugin..." -wp plugin activate query-monitor --path=/var/www/html +# ═════════════════════════════════════════════════════════════════════════════ +# INSTALL — first run on an empty database +# ═════════════════════════════════════════════════════════════════════════════ +if [ "$MODE" = install ]; then + log "No WordPress installation found — starting first-time setup." -echo "[wpcli] Activating Gustavo Portfolio Core dependencies..." -wp plugin activate fluentform fluent-crm fluent-smtp --path=/var/www/html + log "Installing WordPress core at $SITE_URL..." + wp_run core install \ + --url="$SITE_URL" \ + --title="$ADMIN_TITLE" \ + --admin_user="$ADMIN_USER" \ + --admin_password="$ADMIN_PASSWORD" \ + --admin_email="$ADMIN_EMAIL" \ + --skip-email -echo "[wpcli] Activating Gustavo Portfolio Core plugin..." -wp plugin activate gustavoo-portfolio-core --path=/var/www/html + log "Setting site URL to $SITE_URL..." + wp_run option update home "$SITE_URL" + wp_run option update siteurl "$SITE_URL" +# ═════════════════════════════════════════════════════════════════════════════ +# UPDATE — the site already exists, keep every piece of data +# ═════════════════════════════════════════════════════════════════════════════ +else + log "Existing installation detected — keeping database, content and settings." +fi -echo "[wpcli] Activating Gustavo Portfolio theme..." -wp theme activate gustavoo-portfolio --path=/var/www/html +# ─── Language (both modes) ─────────────────────────────────────────────────── +if ! wp_run language core is-installed "$WP_LOCALE" 2>/dev/null; then + log "Installing $WP_LOCALE language pack..." + wp_run language core install "$WP_LOCALE" +fi + +if [ "$(wp_run option get WPLANG 2>/dev/null || true)" != "$WP_LOCALE" ]; then + log "Activating $WP_LOCALE site language..." + wp_run language core activate "$WP_LOCALE" +fi + +# ─── Redis constants (both modes) ──────────────────────────────────────────── +# Values from .env override wp-config.php; anything empty keeps the current +# value, so a manual edit is never lost. +REDIS_HOST="${WP_REDIS_HOST:-}" +REDIS_PORT="${WP_REDIS_PORT:-}" +REDIS_PASSWORD="${WP_REDIS_PASSWORD:-}" +REDIS_DATABASE="${WP_REDIS_DATABASE:-}" + +if [ -n "$REDIS_HOST" ]; then + wp_run config set WP_REDIS_HOST "$REDIS_HOST" --type=constant >/dev/null +fi + +if [ -n "$REDIS_PORT" ]; then + wp_run config set WP_REDIS_PORT "$REDIS_PORT" --type=constant --raw >/dev/null +fi + +if [ -n "$REDIS_PASSWORD" ]; then + wp_run config set WP_REDIS_PASSWORD "$REDIS_PASSWORD" --type=constant >/dev/null +fi + +if [ -n "$REDIS_DATABASE" ]; then + wp_run config set WP_REDIS_DATABASE "$REDIS_DATABASE" --type=constant --raw >/dev/null +fi + +# ═════════════════════════════════════════════════════════════════════════════ +# UPDATES — only on an existing installation, only when an update exists. +# Uses the WordPress upgrader so database migrations run as on the dashboard. +# ═════════════════════════════════════════════════════════════════════════════ +update_core() { + UPDATE_JSON="$(wp_run core check-update --format=json 2>/dev/null || echo '[]')" + + case "$UPDATE_JSON" in + ''|'[]'|'null') + log "WordPress core is up to date ($(wp_run core version 2>/dev/null))." + return 0 + ;; + esac + + # no --minor: security/latest release is the desired behaviour, and the + # major upgrade keeps content and database intact through wp-cli. + log "New WordPress version available — updating core..." + wp_run core update || warn "core update failed" +} + +update_plugins() { + # Only plugins that exist locally are updated; nothing new is downloaded. + STALE="$(wp_run plugin list --status=active --fields=name,update --format=csv 2>/dev/null \ + | awk -F, 'NR>1 && $2 != "" && $2 != "none" && $2 != "available" {print $1}')" + + if [ -z "$STALE" ]; then + log "All active plugins are up to date." + return 0 + fi + + for plugin in $STALE; do + log "Updating plugin '$plugin'..." + wp_run plugin update "$plugin" || warn "could not update plugin '$plugin'" + done +} + +if [ "$MODE" = update ] && [ -z "${WP_SKIP_UPDATES:-}" ]; then + update_core + update_plugins +fi + +# ─── Extra plugins from WordPress.org (both modes) ─────────────────────────── +# These plugins are not versioned in Git: the setup installs them from the +# WordPress.org directory so the repository stays small. Only installed when +# missing: an update is handled by the update step, so a local version or +# manual change is never overwritten here. +INSTALLED_ANY=0 +FAILED_PLUGINS="" + +for plugin in $WP_EXTRA_PLUGINS; do + if wp_run plugin is-installed "$plugin" 2>/dev/null; then + continue + fi + log "Installing plugin '$plugin' from WordPress.org..." + if wp_run plugin install "$plugin"; then + INSTALLED_ANY=1 + else + warn "could not install plugin '$plugin'" + FAILED_PLUGINS="$FAILED_PLUGINS $plugin" + fi +done + +# ─── Plugins (both modes) ──────────────────────────────────────────────────── +# Every plugin in the bind-mounted wp-content/plugins folder is the desired set: +# new folders are activated, missing ones are never downloaded. Plugins bundled +# with WordPress that are not part of the project are removed. +FORBIDDEN_PATTERN="${FORBIDDEN_PATTERN:-^akismet$|^hello$}" + +if [ -n "${WP_ALLOW_FORBIDDEN_PLUGINS:-}" ]; then + FORBIDDEN_PATTERN="" +fi + +# Drop-ins (db.php, object-cache.php, ...) live in wp-content and are managed +# by their parent plugin, they are never activated directly. +is_dropin() { + wp_run plugin list --field=name --status=dropin 2>/dev/null | grep -qx "$1" +} + +log "Activating plugins found in $WP_PATH/wp-content/plugins..." +for plugin in $(wp_run plugin list --field=name --skip-plugins --skip-themes 2>/dev/null || wp_run plugin list --field=name); do + if [ -n "$FORBIDDEN_PATTERN" ] && printf '%s' "$plugin" | grep -Eq "$FORBIDDEN_PATTERN"; then + continue + fi + if is_dropin "$plugin"; then + continue + fi + if ! wp_run plugin is-active "$plugin" 2>/dev/null; then + log "Activating plugin '$plugin'..." + wp_run plugin activate "$plugin" || warn "could not activate '$plugin'" + fi +done + +for plugin in $(wp_run plugin list --field=name --skip-plugins --skip-themes 2>/dev/null || wp_run plugin list --field=name); do + if [ -z "$FORBIDDEN_PATTERN" ]; then + break + fi + if printf '%s' "$plugin" | grep -Eq "$FORBIDDEN_PATTERN"; then + log "Removing unwanted plugin '$plugin'..." + wp_run plugin deactivate "$plugin" 2>/dev/null || true + wp_run plugin uninstall "$plugin" 2>/dev/null || true + wp_run plugin delete "$plugin" 2>/dev/null || true + fi +done + +# ─── Post Views Counter (first install only) ───────────────────────────────── +# Counts views for posts and pages, with manual placement so the theme decides +# where the counter appears (shortcode [post-views]). Existing settings are +# never changed: this only runs on a fresh installation. +if [ "$MODE" = install ] && wp_run plugin is-active post-views-counter 2>/dev/null; then + log "Configuring Post Views Counter (posts + pages, manual placement)..." + + wp_run eval ' + $general = get_option( "post_views_counter_settings_general", array() ); + if ( ! is_array( $general ) ) { + $general = array(); + } + $types = isset( $general["post_types_count"] ) && is_array( $general["post_types_count"] ) + ? $general["post_types_count"] + : array( "post" ); + foreach ( array( "post", "page" ) as $type ) { + if ( ! in_array( $type, $types, true ) ) { + $types[] = $type; + } + } + $general["post_types_count"] = array_values( $types ); + update_option( "post_views_counter_settings_general", $general ); + + $display = get_option( "post_views_counter_settings_display", array() ); + if ( ! is_array( $display ) ) { + $display = array(); + } + $display["position"] = "manual"; + update_option( "post_views_counter_settings_display", $display ); + + $pvc = Post_Views_Counter(); + if ( isset( $pvc->options ) ) { + $pvc->options["general"]["post_types_count"] = $general["post_types_count"]; + $pvc->options["display"]["position"] = "manual"; + } + echo "Post Views Counter: post_types=[post,page] position=[manual]" . PHP_EOL; + ' || warn "could not configure Post Views Counter" +fi + +# Rank Math SEO and Site Kit are installed and activated only; no wizard, SEO +# settings, Search Console or Analytics connection is configured here. + +# ─── Redis object cache (both modes) ───────────────────────────────────────── +if ! wp_run plugin is-installed redis-cache 2>/dev/null; then + log "Installing Redis Object Cache plugin..." + wp_run plugin install redis-cache +fi + +if ! wp_run plugin is-active redis-cache 2>/dev/null; then + log "Activating Redis Object Cache plugin..." + wp_run plugin activate redis-cache || warn "could not activate redis-cache" +fi + +# The drop-in (wp-content/object-cache.php) is what actually routes the cache to +# Redis; it is (re)created when missing, stale or disabled. +if ! wp_run redis status 2>/dev/null | grep -qE '^[[:space:]]*Drop-in:[[:space:]]+Valid'; then + log "Enabling Redis object cache drop-in..." + wp_run redis enable >/dev/null 2>&1 || wp_run redis update-dropin >/dev/null 2>&1 || true +fi + +if [ -n "$REDIS_HOST" ]; then + REDIS_STATUS="$(wp_run redis status 2>/dev/null || true)" + + # The status output may lead with the "Drop-ins: [...]" block, so the checks + # match the key line anywhere in the output. + if printf '%s' "$REDIS_STATUS" | grep -qE '^[[:space:]]*Status:[[:space:]]+Connected'; then + log "Redis object cache connected." + else + warn "Redis object cache could not connect — check the redis service." + fi + + if printf '%s' "$REDIS_STATUS" | grep -qE '^[[:space:]]*Drop-in:[[:space:]]+Valid'; then + log "Redis drop-in (wp-content/object-cache.php) is valid." + else + warn "Redis drop-in is missing or invalid — re-run 'docker compose restart wpcli'." + fi +fi + +# ─── Theme (both modes) ────────────────────────────────────────────────────── +if ! wp_run theme is-installed "$WP_THEME" 2>/dev/null; then + warn "theme '$WP_THEME' is not installed in wp-content/themes — skipping activation." +else + if ! wp_run theme is-active "$WP_THEME" 2>/dev/null; then + log "Activating $WP_THEME theme..." + wp_run theme activate "$WP_THEME" || warn "could not activate theme '$WP_THEME'" + fi +fi + +# Only project themes stay available: every other theme (the WordPress defaults) +# is removed. WP_KEEP_THEMES lists themes to preserve on purpose. +log "Removing leftover themes..." +for theme in $(wp_run theme list --field=name 2>/dev/null); do + if [ "$theme" = "$WP_THEME" ]; then + continue + fi + if [ -n "${WP_KEEP_THEMES:-}" ] && printf '%s' " $WP_KEEP_THEMES " | grep -q " $theme "; then + continue + fi + if wp_run theme is-active "$theme" 2>/dev/null; then + continue + fi + log "Removing theme '$theme'..." + wp_run theme delete "$theme" || warn "could not delete theme '$theme'" +done + +# ─── Project seeder (both modes, idempotent) ───────────────────────────────── +# Only fills in what is missing (pages, forms, feed, widget defaults). Content +# edited by the administrator is never overwritten. +if wp_run plugin is-active gustavoo-portfolio-core 2>/dev/null; then + log "Running portfolio seeder..." + wp_run eval 'GSO_Seeder::maybe_seed();' || warn "seeder failed" + log "Seeder state: $(wp_run option get gso_portfolio_seed_state --format=json 2>/dev/null || echo '{}')" +fi + +log "Setup complete ($MODE). Site available at $SITE_URL" + +# A plugin that could not be installed leaves the site without a feature the +# project expects (SEO, analytics, view counts). Fail loudly instead of +# reporting success: the container is restarted by the operator and the next +# run simply tries again, since installation is idempotent. +if [ -n "$FAILED_PLUGINS" ]; then + warn "Required plugins are missing:$FAILED_PLUGINS" + warn "Check network access to downloads.wordpress.org and restart the container." + exit 1 +fi diff --git a/wp-content/plugins/automatic-ai-posts/admin/admin.css b/wp-content/plugins/automatic-ai-posts/admin/admin.css new file mode 100644 index 0000000..56ea77a --- /dev/null +++ b/wp-content/plugins/automatic-ai-posts/admin/admin.css @@ -0,0 +1,1712 @@ +/** + * Automatic AI Posts — painel administrativo + * + * Todas as regras são escopadas em #aap-app para não afetar o restante do wp-admin. + */ + +#aap-app { + --aap-navy: #0f2038; + --aap-navy-2: #132a47; + --aap-blue: #2563eb; + --aap-blue-hover: #1d4ed8; + --aap-blue-dark: #1e40af; + --aap-blue-soft: #eff6ff; + --aap-border: #e2e8f0; + --aap-border-soft: #eef2f7; + --aap-bg: #f8fafc; + --aap-text: #0f172a; + --aap-muted: #64748b; + --aap-muted-2: #94a3b8; + --aap-green: #16a34a; + --aap-green-soft: #ecfdf3; + --aap-green-line: #bbf7d0; + --aap-red: #dc2626; + --aap-amber: #b45309; + + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif; + color: var(--aap-text); + font-size: 15px; + line-height: 1.55; + margin: 0; + -webkit-font-smoothing: antialiased; +} + +/* O painel ocupa toda a largura da área de conteúdo */ +#aap-app ~ .clear { display: none; } +body.no-js #aap-app .aap-panel { display: block; } + +/* Layout em tela cheia nesta página: sem limite de largura e sem o respiro + que o WordPress adiciona ao redor do conteúdo */ +body.toplevel_page_automatic-ai-posts #wpbody-content > .wrap, +body.toplevel_page_automatic-ai-posts #wpbody-content > .wrap > .wrap { + max-width: none; + margin: 0; + padding: 0; +} + +body.toplevel_page_automatic-ai-posts #wpbody-content { padding: 0; margin: 0; } + +/* O padding-left de 20px do #wpcontent deixa uma faixa cinza antes do painel. + É zerado, e o `box-sizing` mantém o conteúdo na largura correta — o recorte + lateral da barra do WordPress continua funcionando. */ +body.toplevel_page_automatic-ai-posts #wpcontent { + padding-left: 0; + box-sizing: border-box; +} + +/* Rodapé do WordPress oculto nesta página */ +body.toplevel_page_automatic-ai-posts #wpfooter { display: none; } + +/* Avisos de outros plugins não aparecem nesta página (quebram o layout do painel) */ +body.toplevel_page_automatic-ai-posts #wpbody-content .notice, +body.toplevel_page_automatic-ai-posts #wpbody-content .update-nag { + display: none !important; +} + +/* ─── Shell: sidebar + conteúdo ─────────────────────────────────────────── */ + +#aap-app .aap-shell { + display: grid; + grid-template-columns: 236px minmax(0, 1fr); + gap: 0; + align-items: stretch; +} + +/* ─── Sidebar ───────────────────────────────────────────────────────────── */ + +/* A coluna navy acompanha a altura do conteúdo (sem emenda visual), mas o + conteúdo dela fica preso na tela: a navegação não rola junto com a página. */ +#aap-app .aap-sidebar { + background: var(--aap-navy); + border-radius: 0; + padding: 0; + box-sizing: border-box; + min-width: 0; +} + +#aap-app .aap-sidebar-inner { + position: sticky; + top: 32px; + z-index: 10; + display: flex; + flex-direction: column; + height: calc(100vh - 32px); + padding: 18px 14px 14px; + box-sizing: border-box; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, .18) transparent; +} + +#aap-app .aap-sidebar-inner::-webkit-scrollbar { width: 6px; } +#aap-app .aap-sidebar-inner::-webkit-scrollbar-track { background: transparent; } +#aap-app .aap-sidebar-inner::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, .18); + border-radius: 3px; +} + +#aap-app .aap-brand { + display: flex; + align-items: center; + gap: 10px; + padding: 0 6px 22px; +} + +#aap-app .aap-brand-icon { + width: 38px; + height: 38px; + display: grid; + place-items: center; + flex: 0 0 38px; + color: #60a5fa; +} + +#aap-app .aap-brand-icon svg { width: 100%; height: 100%; display: block; } + +#aap-app .aap-brand-title { + color: #fff; + font-size: 15px; + line-height: 1.15; + font-weight: 700; + letter-spacing: -0.2px; +} + +#aap-app .aap-nav { display: flex; flex-direction: column; gap: 4px; } + +#aap-app .aap-nav-item { + display: flex; + align-items: center; + gap: 12px; + padding: 11px 14px; + border-radius: 8px; + color: #cbd5e1; + text-decoration: none; + font-size: 13.5px; + font-weight: 500; + transition: background .15s ease, color .15s ease; + box-shadow: none; +} + +#aap-app .aap-nav-item .aap-nav-icon { + display: inline-flex; + width: 18px; + height: 18px; + flex: 0 0 18px; + color: #94a3b8; + transition: color .15s ease; +} + +#aap-app .aap-nav-item .aap-nav-icon svg { width: 18px; height: 18px; display: block; } + +#aap-app .aap-nav-item:hover, +#aap-app .aap-nav-item:focus { + background: rgba(255, 255, 255, .06); + color: #fff; + box-shadow: none; +} + +#aap-app .aap-nav-item:hover .aap-nav-icon, +#aap-app .aap-nav-item:focus .aap-nav-icon { color: #e2e8f0; } + +#aap-app .aap-nav-item.aap-active, +#aap-app .aap-nav-item.aap-active:hover, +#aap-app .aap-nav-item.aap-active:focus { + background: var(--aap-blue); + color: #fff; + font-weight: 600; +} + +#aap-app .aap-nav-item.aap-active .aap-nav-icon { color: #fff; } + +#aap-app .aap-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--aap-muted-2); + flex: 0 0 8px; + display: inline-block; +} + +#aap-app .aap-dot.is-on { background: #22c55e; box-shadow: 0 0 0 3px rgba(34, 197, 94, .18); } + +/* ─── Painel principal ──────────────────────────────────────────────────── */ + +#aap-app .aap-main { min-width: 0; } + +#aap-app .aap-panel { + background: #fff; + border: 1px solid var(--aap-border); + border-left: 0; + border-radius: 0; + padding: 18px 20px 22px; + box-shadow: none; + min-height: calc(100vh - 90px); + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: clip; +} + +#aap-app .aap-panel[hidden] { display: none !important; } + +/* O conteúdo preenche a altura restante do painel (que é flex em coluna) */ +#aap-app .aap-panel > #aap-settings-form, +#aap-app .aap-panel > .aap-cards, +#aap-app .aap-panel > .aap-log-shell { flex: 1 1 auto; } + +/* ─── Cabeçalho fixo de cada aba ───────────────────────────────────────── */ +/* Fica sempre visível no topo, alinhado logo abaixo da barra do WordPress + (o offset exato é calculado em admin.js). A sidebar não é fixa. */ + +#aap-app .aap-sticky-head { + position: sticky; + top: 32px; + z-index: 15; + background: #fff; + margin: -18px -20px 0; + padding: 18px 20px 6px; + border-radius: 0; + transition: box-shadow .15s ease; +} + +#aap-app .aap-sticky-head::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -1px; + height: 1px; + background: var(--aap-border); + opacity: 0; + transition: opacity .15s ease; +} + +#aap-app .aap-sticky-head.is-stuck { + box-shadow: 0 6px 14px -6px rgba(15, 23, 42, .28); +} + +/* Linha de corte do cabeçalho: deixa claro que o conteúdo passa por baixo */ +#aap-app .aap-sticky-head.is-stuck::after { opacity: 1; } + +#aap-app .aap-sticky-head > .aap-panel-head, +#aap-app .aap-sticky-head > .aap-settings-head { margin-bottom: 16px; } + +#aap-app .aap-sticky-head ~ * { scroll-margin-top: 260px; } + +/* ─── Cabeçalho do painel ──────────────────────────────────────────────── */ + +#aap-app .aap-panel-head { + display: flex; + align-items: flex-start; + gap: 14px; + margin-bottom: 18px; +} + +#aap-app .aap-panel-head-text { flex: 1; min-width: 0; } + +#aap-app .aap-title { + display: flex; + align-items: center; + gap: 12px; + margin: 0; + padding: 0; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.4px; + line-height: 1.25; + color: var(--aap-text); +} + +#aap-app .aap-title svg { width: 24px; height: 24px; color: var(--aap-blue); flex: 0 0 24px; } + +#aap-app .aap-subtitle { + margin: 4px 0 0; + font-size: 13px; + color: var(--aap-muted); +} + +/* Ícone redondo colorido (cabeçalhos de card, painéis) */ +#aap-app .aap-ico { + display: inline-grid; + place-items: center; + width: 36px; + height: 36px; + border-radius: 50%; + flex: 0 0 36px; + background: #eef2ff; + color: var(--aap-blue); +} + +#aap-app .aap-ico svg { width: 18px; height: 18px; display: block; } + +#aap-app .aap-ico--blue { background: #dbeafe; color: #1d4ed8; } +#aap-app .aap-ico--violet { background: #ede9fe; color: #7c3aed; } +#aap-app .aap-ico--green { background: #dcfce7; color: #15803d; } +#aap-app .aap-ico--orange { background: #ffedd5; color: #ea580c; } +#aap-app .aap-ico--teal { background: #ccfbf1; color: #0d9488; } +#aap-app .aap-ico--purple { background: #e9d5ff; color: #9333ea; } +#aap-app .aap-ico--lg { width: 42px; height: 42px; flex: 0 0 42px; border-radius: 12px; } +#aap-app .aap-ico--lg svg { width: 21px; height: 21px; } + +/* ─── Botões ────────────────────────────────────────────────────────────── */ + +#aap-app .aap-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + height: auto; + padding: 8px 16px; + border-radius: 8px; + border: 1px solid transparent; + font-size: 13px; + font-weight: 600; + line-height: 1.4; + text-decoration: none; + cursor: pointer; + background: #fff; + color: var(--aap-text); + box-shadow: none; + transition: background .15s ease, border-color .15s ease, color .15s ease; +} + +#aap-app .aap-btn svg { width: 15px; height: 15px; } + +#aap-app .aap-btn-primary, +#aap-app .aap-btn-primary:visited { + background: var(--aap-blue); + border-color: var(--aap-blue); + color: #fff; +} + +#aap-app .aap-btn-primary:hover, +#aap-app .aap-btn-primary:focus { + background: var(--aap-blue-hover); + border-color: var(--aap-blue-hover); + color: #fff; + box-shadow: none; +} + +#aap-app .aap-btn-outline, +#aap-app .aap-btn-outline:visited { + background: #fff; + border-color: #cbd5e1; + color: var(--aap-blue-dark); +} + +#aap-app .aap-btn-outline:hover, +#aap-app .aap-btn-outline:focus { + background: var(--aap-blue-soft); + border-color: #93c5fd; + color: var(--aap-blue-dark); + box-shadow: none; +} + +#aap-app .aap-btn-danger-outline, +#aap-app .aap-btn-danger-outline:visited { + background: #fff; + border-color: #fca5a5; + color: var(--aap-red); +} + +#aap-app .aap-btn-danger-outline:hover, +#aap-app .aap-btn-danger-outline:focus { + background: #fef2f2; + border-color: #f87171; + color: #b91c1c; + box-shadow: none; +} + +#aap-app .aap-btn:disabled, +#aap-app .aap-btn[disabled] { + opacity: .6; + cursor: not-allowed; +} + +/* ─── Masthead da aba Configurações ─────────────────────────────────────── */ + +#aap-app .aap-settings-head { + display: flex; + align-items: flex-start; + gap: 16px; + margin-bottom: 18px; +} + +#aap-app .aap-settings-head .aap-title { font-size: 24px; } + +/* ─── Botões de categoria (âncoras) ────────────────────────────────────── */ + +/* Área de conteúdo do painel (cabeçalho fica fora dela) */ +#aap-app .aap-panel-body { min-height: 0; } + +#aap-app .aap-panel-body > :first-child { margin-top: 0; } + +#aap-app .aap-cat-links { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 0; + background: none; + border: 0; + border-radius: 0; + margin-bottom: 22px; +} + +#aap-app .aap-cat-links a, +#aap-app .aap-cat-links a:visited { + display: inline-flex; + align-items: center; + padding: 8px 15px; + border: 1px solid #d7e2f5; + border-radius: 8px; + background: #f4f8ff; + color: #1e40af; + font-size: 14px; + font-weight: 600; + line-height: 1.4; + text-decoration: none; + transition: background .15s ease, border-color .15s ease, color .15s ease; +} + +#aap-app .aap-cat-links a:hover, +#aap-app .aap-cat-links a:focus { + background: #e8f0ff; + border-color: #a9c5f5; + color: var(--aap-blue); + box-shadow: none; +} + +#aap-app .aap-cat-links a.is-active, +#aap-app .aap-cat-links a.is-active:hover { + background: var(--aap-blue); + border-color: var(--aap-blue); + color: #fff; +} + +#aap-app .aap-cat-links a.is-flash { animation: aap-flash 1.4s ease; } + +@keyframes aap-flash { + 0%, 100% { transform: none; } + 25% { transform: translateY(-1px); box-shadow: 0 4px 10px -4px rgba(37, 99, 235, .5); } +} + +/* ─── Grid de cards ─────────────────────────────────────────────────────── */ + +#aap-app .aap-cards { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 12px; + align-items: start; +} + +#aap-app .aap-card { + background: #fff; + border: 1px solid var(--aap-border); + border-radius: 10px; + box-shadow: 0 1px 2px rgba(15, 23, 42, .04); + overflow: hidden; + scroll-margin-top: 260px; +} + +#aap-app .aap-card-head { + display: flex; + align-items: center; + gap: 14px; + width: 100%; + padding: 14px 18px; + margin: 0; + background: none; + border: 0; + border-radius: 0; + text-align: left; + color: inherit; + box-shadow: none; + font-family: inherit; +} + +#aap-app .aap-card-head:hover { background: #fbfdff; box-shadow: none; } + +#aap-app .aap-card-head:focus-visible { + outline: 2px solid var(--aap-blue); + outline-offset: -2px; +} + +#aap-app .aap-card-heading { flex: 1; min-width: 0; } + +#aap-app .aap-card-title { + margin: 0; + padding: 0; + font-size: 17px; + font-weight: 700; + color: var(--aap-text); + line-height: 1.3; +} + +#aap-app .aap-card-desc { + margin: 3px 0 0; + font-size: 13.5px; + color: var(--aap-muted); + line-height: 1.45; +} + +#aap-app .aap-card-body { padding: 2px 18px 16px; } + +/* ─── Layout do card (título/descrição em linhas próprias) ──────────────── */ + +#aap-app .aap-card-heading { display: block; } + +#aap-app .aap-card-title { display: block; } + +#aap-app .aap-card-desc { display: block; } + +/* ─── Campos ────────────────────────────────────────────────────────────── */ + +#aap-app .aap-field { + display: grid; + grid-template-columns: 190px minmax(0, 1fr); + gap: 8px 22px; + align-items: start; + padding: 9px 0; +} + +#aap-app .aap-field > * { min-width: 0; } + +#aap-app .aap-field > label, +#aap-app .aap-field > .aap-label { + font-size: 14px; + font-weight: 700; + color: #334155; + padding-top: 8px; + line-height: 1.4; +} + +#aap-app .aap-field-full { grid-template-columns: minmax(0, 1fr); } +#aap-app .aap-field-full > label { padding-top: 0; padding-bottom: 3px; } +#aap-app .aap-field-full .aap-help { margin-top: 5px; } + +#aap-app .aap-field-input { min-width: 0; } + +#aap-app .aap-help { + margin: 6px 0 0; + font-size: 13px; + line-height: 1.5; + color: var(--aap-muted); +} + +#aap-app .aap-help:first-child { margin-top: 0; } + +#aap-app .aap-help code { + font-size: 12px; + background: #f1f5f9; + color: #0f172a; + padding: 1px 5px; + border-radius: 4px; + border: 1px solid #e2e8f0; +} + +#aap-app .aap-help a { color: var(--aap-blue); text-decoration: none; font-weight: 500; } +#aap-app .aap-help a:hover { text-decoration: underline; } + +/* Inputs, selects e textareas */ +#aap-app input[type="text"], +#aap-app input[type="password"], +#aap-app input[type="number"], +#aap-app input[type="datetime-local"], +#aap-app select, +#aap-app textarea { + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; + min-height: 40px; + height: auto; + margin: 0; + padding: 8px 12px; + border: 1px solid #cbd5e1; + border-radius: 7px; + background-color: #fff; + color: var(--aap-text); + font-size: 14.5px; + font-family: inherit; + line-height: 1.45; + box-shadow: inset 0 1px 1px rgba(15, 23, 42, .03); + transition: border-color .15s ease, box-shadow .15s ease; +} + +#aap-app select { + padding-right: 32px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 4.5 6 8.5 10 4.5' fill='none' stroke='%2364748b' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 11px center; + background-size: 12px; + -webkit-appearance: none; + appearance: none; +} + +#aap-app textarea { resize: vertical; min-height: 76px; line-height: 1.55; } + +#aap-app input[type="text"]:focus, +#aap-app input[type="password"]:focus, +#aap-app input[type="number"]:focus, +#aap-app input[type="datetime-local"]:focus, +#aap-app select:focus, +#aap-app textarea:focus { + border-color: var(--aap-blue); + box-shadow: 0 0 0 3px rgba(37, 99, 235, .15); + outline: none; +} + +#aap-app input[type="number"] { max-width: 110px; } + +#aap-app .aap-inline { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 10px; +} + +#aap-app .aap-inline > .aap-radio, +#aap-app .aap-inline > .aap-check { flex: 0 0 auto; } + +#aap-app .aap-inline .aap-inline-label { + font-size: 12.5px; + font-weight: 500; + color: #475569; +} + +#aap-app .aap-inline input[type="number"] { width: 64px; min-width: 64px; max-width: 64px; flex: 0 0 auto; } +#aap-app .aap-inline input[type="number"].aap-w-70 { width: 70px; min-width: 70px; max-width: 70px; } +#aap-app .aap-inline select { width: auto; min-width: 0; flex: 0 1 auto; } +#aap-app .aap-inline .aap-help { flex: 1 1 100%; } +#aap-app .aap-inline > .aap-btn { flex: 0 0 auto; } + +/* Radios e checkboxes */ +#aap-app .aap-radio, +#aap-app .aap-check { + display: inline-flex; + align-items: center; + gap: 7px; + flex-wrap: nowrap; + font-size: 13px; + font-weight: 400; + color: #334155; + cursor: pointer; + margin: 0; + white-space: nowrap; +} + +#aap-app .aap-radio > span, +#aap-app .aap-check > span { white-space: nowrap; } + +#aap-app input[type="radio"], +#aap-app input[type="checkbox"] { + margin: 0; + width: 16px; + height: 16px; + min-width: 16px; + max-width: 16px; + flex: 0 0 16px; + border: 1px solid #94a3b8; + background: #fff; + box-shadow: none; + -webkit-appearance: none; + appearance: none; + position: relative; + display: inline-block; + vertical-align: middle; + cursor: pointer; +} + +#aap-app input[type="radio"] { border-radius: 50%; } +#aap-app input[type="checkbox"] { border-radius: 4px; } + +/* O WordPress injeta um ::before branco fora de centro no radio marcado, + que aparecia como um "luar". O indicador é desenhado aqui. */ +#aap-app input[type="radio"]::before { content: none; } + +#aap-app input[type="radio"]:checked { + border-color: var(--aap-blue); + background: var(--aap-blue); + box-shadow: none; +} + +#aap-app input[type="radio"]:checked::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + display: block; + width: 6px; + height: 6px; + margin: 0; + border-radius: 50%; + background: #fff; + transform: translate(-50%, -50%); +} + +#aap-app input[type="checkbox"]:checked { + border-color: var(--aap-blue); + background: var(--aap-blue); +} + +#aap-app input[type="checkbox"]:checked::before { + content: ""; + position: absolute; + inset: 0; + margin: 0; + width: auto; + height: auto; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 10.5 8.5 14 15 6.5' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/13px no-repeat; +} + +#aap-app input[type="radio"]:focus-visible, +#aap-app input[type="checkbox"]:focus-visible { + outline: 2px solid var(--aap-blue); + outline-offset: 2px; +} + +#aap-app .aap-check-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px 12px; +} + +#aap-app .aap-check-grid .aap-check { + font-size: 12.5px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +#aap-app .aap-check-grid .aap-check input { flex: 0 0 16px; } + +/* Divisor interno com rótulo */ +#aap-app .aap-divider { + display: flex; + align-items: center; + gap: 10px; + margin: 14px 0 4px; + padding-top: 14px; + border-top: 1px dashed var(--aap-border); + font-size: 10.5px; + font-weight: 700; + letter-spacing: .9px; + text-transform: uppercase; + color: var(--aap-muted-2); +} + +#aap-app .aap-field:first-child + .aap-divider { margin-top: 0; border-top: none; padding-top: 0; } +#aap-app .aap-card-body > .aap-divider:first-child { display: none; } + +/* Aviso flutuante de configurações salvas */ +#aap-app .aap-toast { + position: fixed; + right: 28px; + bottom: calc(var(--aap-footer-h, 48px) + 16px); + z-index: 101; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 11px 16px; + background: var(--aap-green-soft); + border: 1px solid var(--aap-green-line); + border-radius: 10px; + color: #15803d; + font-size: 13px; + font-weight: 600; + box-shadow: 0 16px 32px -18px rgba(15, 23, 42, .45); + animation: aap-float-in .18s ease; +} + +#aap-app .aap-toast[hidden] { display: none; } +#aap-app .aap-toast svg { width: 16px; height: 16px; flex: 0 0 16px; } + +/* Barra flutuante de alterações não salvas */ +#aap-app .aap-save-float { + position: fixed; + right: 28px; + bottom: calc(var(--aap-footer-h, 48px) + 16px); + z-index: 100; + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + background: #fff; + border: 1px solid var(--aap-border); + border-radius: 12px; + box-shadow: 0 18px 38px -18px rgba(15, 23, 42, .45), 0 2px 6px rgba(15, 23, 42, .08); + animation: aap-float-in .18s ease; +} + +#aap-app .aap-save-float[hidden] { display: none; } + +#aap-app .aap-save-float-note { + font-size: 12.5px; + font-weight: 600; + color: var(--aap-muted); + margin-right: 4px; + white-space: nowrap; +} + +@keyframes aap-float-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: none; } +} + +@media (max-width: 782px) { + #aap-app .aap-save-float, + #aap-app .aap-toast { + left: 12px; + right: 12px; + bottom: 12px; + } + #aap-app .aap-save-float { flex-wrap: wrap; } + #aap-app .aap-save-float-note { flex: 1 1 100%; } + #aap-app .aap-save-float .aap-btn { flex: 1 1 auto; justify-content: center; } +} + +/* ─── Rodapé da aba Ajuda ───────────────────────────────────────────────── */ + +#aap-app .aap-meta { + display: flex; + flex-wrap: wrap; + gap: 8px 22px; + font-size: 12px; + color: var(--aap-muted); +} + +#aap-app .aap-meta kbd { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 11px; + padding: 1px 6px; + border: 1px solid var(--aap-border); + border-bottom-width: 2px; + border-radius: 4px; + background: var(--aap-bg); + color: #334155; +} + +/* ─── Execuções ─────────────────────────────────────────────────────────── */ + +#aap-app .aap-actions { display: flex; gap: 10px; margin-left: auto; flex: 0 0 auto; } + +#aap-app .aap-exec-status { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 10px; + background: var(--aap-green-soft); + border: 1px solid var(--aap-green-line); + border-radius: 10px; + padding: 14px 16px; + margin-bottom: 18px; +} + +#aap-app .aap-exec-status > .aap-dot { align-self: center; } + +#aap-app .aap-exec-status .aap-dot { + margin-top: 6px; + background: #16a34a; + box-shadow: 0 0 0 3px rgba(22, 163, 74, .16); +} + +#aap-app .aap-exec-status.is-idle { + background: var(--aap-bg); + border-color: var(--aap-border); +} + +#aap-app .aap-exec-status.is-idle .aap-dot { + background: var(--aap-muted-2); + box-shadow: none; +} + +#aap-app .aap-exec-status.is-aborting { + background: #fffbeb; + border-color: #fde68a; +} + +#aap-app .aap-exec-status.is-aborting .aap-dot { + background: #f59e0b; + box-shadow: 0 0 0 3px rgba(245, 158, 11, .18); +} + +#aap-app .aap-exec-main { flex: 1 1 320px; min-width: 0; } + +#aap-app .aap-exec-title { + font-size: 15px; + font-weight: 700; + color: #14532d; + line-height: 1.3; +} + +#aap-app .aap-exec-status.is-idle .aap-exec-title { color: #334155; } +#aap-app .aap-exec-status.is-aborting .aap-exec-title { color: #92400e; } + +#aap-app .aap-exec-sub { + margin-top: 3px; + font-size: 12.5px; + color: var(--aap-muted); +} + +#aap-app .aap-exec-side { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; + flex: 0 0 auto; +} + +#aap-app .aap-progress { + position: relative; + height: 8px; + border-radius: 99px; + background: #e2e8f0; + overflow: hidden; + margin-top: 10px; +} + +#aap-app .aap-progress-bar { + position: absolute; + inset: 0 auto 0 0; + width: 0; + max-width: 100%; + border-radius: 99px; + background: var(--aap-blue); + transition: width .4s ease; +} + +#aap-app .aap-progress.is-indeterminate .aap-progress-bar { + width: 35% !important; + animation: aap-indeterminate 1.4s ease-in-out infinite; +} + +@keyframes aap-indeterminate { + 0% { left: -35%; } + 100% { left: 100%; } +} + +#aap-app .aap-progress-foot { + display: flex; + justify-content: space-between; + gap: 12px; + margin-top: 6px; + font-size: 11.5px; + color: var(--aap-muted); +} + +#aap-app .aap-exec-status.is-idle .aap-progress, +#aap-app .aap-exec-status.is-idle .aap-progress-foot { display: none; } + +#aap-app .aap-exec-note { + grid-area: auto; + margin: 10px 0 0; + font-size: 11.5px; + color: var(--aap-muted-2); + flex: 1 1 100%; +} + +#aap-app .aap-feedback { font-size: 12.5px; } +#aap-app .aap-feedback.is-error { color: var(--aap-red); } +#aap-app .aap-feedback.is-success { color: var(--aap-green); } +#aap-app .aap-feedback.is-warn { color: var(--aap-amber); } +#aap-app .aap-feedback.is-pending { color: var(--aap-muted); } +#aap-app .aap-feedback:empty { display: none; } + +/* Painel interno (últimos posts, automático, execução) */ +#aap-app .aap-subpanel { + border: 1px solid var(--aap-border); + border-radius: 10px; + margin-bottom: 16px; + overflow: hidden; +} + +#aap-app .aap-subpanel-head { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 12px 16px; + background: #fff; + border: 0; + border-bottom: 1px solid var(--aap-border-soft); + margin: 0; + text-align: left; + color: var(--aap-text); + font-family: inherit; + box-shadow: none; + border-radius: 0; +} + +#aap-app .aap-subpanel-head svg.aap-lead { width: 17px; height: 17px; color: #334155; flex: 0 0 17px; } + +#aap-app .aap-subpanel-title { + flex: 1; + font-size: 14px; + font-weight: 700; +} + +#aap-app .aap-subpanel-body { padding: 16px; } + +/* Tabela de últimos posts */ +#aap-app .aap-table-wrap { overflow-x: auto; } + +#aap-app .aap-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 12.5px; + border: 1px solid var(--aap-border); + border-radius: 8px; + overflow: hidden; +} + +#aap-app .aap-table th { + background: #f8fafc; + border-bottom: 1px solid var(--aap-border); + padding: 9px 12px; + text-align: left; + font-size: 11.5px; + font-weight: 700; + color: #475569; + white-space: nowrap; +} + +#aap-app .aap-table td { + padding: 9px 12px; + border-bottom: 1px solid var(--aap-border-soft); + vertical-align: middle; + color: #334155; +} + +#aap-app .aap-table tbody tr:last-child td { border-bottom: 0; } +#aap-app .aap-table tbody tr:hover td { background: #fbfdff; } + +#aap-app .aap-table .aap-thumb { + width: 52px; + height: 36px; + object-fit: cover; + border-radius: 5px; + display: block; + background: #e2e8f0; +} + +#aap-app .aap-table .aap-thumb-empty { + width: 52px; + height: 36px; + border-radius: 5px; + background: #f1f5f9; + border: 1px dashed #cbd5e1; + display: grid; + place-items: center; + color: #94a3b8; +} + +#aap-app .aap-table .aap-thumb-empty svg { width: 15px; height: 15px; } + +#aap-app .aap-table .aap-col-title { font-weight: 600; color: var(--aap-text); } +#aap-app .aap-table .aap-col-title a { color: var(--aap-text); text-decoration: none; } +#aap-app .aap-table .aap-col-title a:hover { color: var(--aap-blue); } + +#aap-app .aap-table .aap-dot { width: 7px; height: 7px; } + +#aap-app .aap-empty { + text-align: center; + padding: 26px 16px; + color: var(--aap-muted); + font-size: 12.5px; +} + +/* ─── Status do plugin (aba Ajuda) ──────────────────────────────────────── */ + +#aap-app .aap-plugin-status { + display: flex; + align-items: center; + gap: 8px; + font-size: 13.5px; + font-weight: 500; + color: var(--aap-text); +} + +#aap-app .aap-plugin-version { + margin-left: auto; + color: var(--aap-muted); + font-weight: 400; +} + +/* ─── Log ───────────────────────────────────────────────────────────────── */ + +#aap-app .aap-log-shell { + background: #0d1117; + border-radius: 10px; + padding: 8px; + border: 1px solid #1f2937; +} + +#aap-app .aap-log-box { + height: min(62vh, 640px); + min-height: 320px; + overflow-y: auto; + padding: 12px 14px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + font-size: 12.5px; + line-height: 1.75; + color: #d4d4d4; + white-space: pre-wrap; + word-break: break-word; + scrollbar-width: thin; + scrollbar-color: #334155 transparent; +} + +#aap-app .aap-log-box::-webkit-scrollbar { width: 10px; } +#aap-app .aap-log-box::-webkit-scrollbar-thumb { background: #334155; border-radius: 99px; } + +#aap-app .aap-log-box .aap-log-line { display: block; } +#aap-app .aap-log-box .aap-log-time { color: #7dd3fc; } +#aap-app .aap-log-box .aap-log-level { font-weight: 700; } +#aap-app .aap-log-box .aap-log-level-info { color: #4ade80; } +#aap-app .aap-log-box .aap-log-level-erro { color: #f87171; } +#aap-app .aap-log-box .aap-log-level-aviso { color: #fbbf24; } +#aap-app .aap-log-box .aap-log-sep { color: #7dd3fc; font-weight: 700; } +#aap-app .aap-log-box .aap-log-empty { color: #64748b; } + +/* Barra superior do log (filtro + status) */ +#aap-app .aap-log-toolbar { + display: flex; + align-items: center; + gap: 12px; + padding: 2px 6px 10px; +} + +#aap-app .aap-log-toolbar select { + background: #111827; + color: #cbd5e1; + border-color: #334155; +} + +#aap-app .aap-log-status { color: #94a3b8; font-size: 12px; } + +#aap-app .aap-log-more { padding: 10px 6px 2px; text-align: center; } + +#aap-app .aap-log-more .aap-btn { + background: #111827; + border-color: #334155; + color: #cbd5e1; +} + +#aap-app .aap-log-more .aap-btn:hover { + background: #1f2937; + border-color: #475569; + color: #fff; +} + +/* Cada linha do log: hora | nível | mensagem */ +#aap-app .aap-log-box .aap-log-line { + display: flex; + align-items: baseline; + gap: 10px; + padding: 2px 0; + border-bottom: 1px solid rgba(148, 163, 184, .07); +} + +#aap-app .aap-log-box .aap-log-time { flex: 0 0 auto; } +#aap-app .aap-log-box .aap-log-level { flex: 0 0 54px; } +#aap-app .aap-log-box .aap-log-msg { flex: 1 1 auto; } + +/* Campos por provedor de IA */ +#aap-app .aap-provider-block[hidden] { display: none; } + +#aap-app .aap-log-foot { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; + padding: 10px 6px 4px; + margin-top: 6px; + position: relative; +} + +#aap-app .aap-log-foot::before { + content: ""; + position: absolute; + top: 0; + left: 6px; + right: 6px; + height: 1px; + background: #1f2937; +} + +#aap-app .aap-log-foot .aap-check { color: #cbd5e1; font-size: 12.5px; } +#aap-app .aap-log-foot .aap-hint-note { color: #94a3b8; } +#aap-app .aap-log-foot .aap-hint-note .aap-dot { box-shadow: 0 0 0 3px rgba(34, 197, 94, .18); } +#aap-app .aap-log-foot .aap-switch-track { background: #334155; } +#aap-app .aap-log-foot .aap-switch input:checked + .aap-switch-track { background: var(--aap-blue); } + +#aap-app .aap-log-foot .aap-log-status { + margin-left: auto; + font-size: 12px; + color: #64748b; +} + +/* Switch (usado na aba de log: atualização automática) */ +#aap-app .aap-switch { position: relative; display: inline-flex; } + +#aap-app .aap-switch input { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + margin: 0; + opacity: 0; + cursor: pointer; + z-index: 2; +} + +#aap-app .aap-switch-track { + width: 40px; + height: 22px; + border-radius: 99px; + background: #cbd5e1; + transition: background .15s ease; + position: relative; + pointer-events: none; +} + +#aap-app .aap-switch-track::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 2px rgba(15, 23, 42, .25); + transition: transform .15s ease; +} + +#aap-app .aap-switch input:checked + .aap-switch-track { background: var(--aap-blue); } +#aap-app .aap-switch input:checked + .aap-switch-track::after { transform: translateX(18px); } +#aap-app .aap-switch input:focus-visible + .aap-switch-track { box-shadow: 0 0 0 3px rgba(37, 99, 235, .3); } +#aap-app .aap-switch input:disabled + .aap-switch-track { opacity: .5; } + +#aap-app .aap-hint-note { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--aap-muted); +} + +#aap-app .aap-hint-note svg { width: 14px; height: 14px; color: #22c55e; } + +/* ─── Responsivo ────────────────────────────────────────────────────────── */ + +/* ─── Layout de aplicação (desktop) ─────────────────────────────────────── */ +/* O cabeçalho fica fora da área de rolagem e o conteúdo rola dentro do painel, + assim nada é cortado por baixo do cabeçalho. */ + +@media screen and (min-width: 783px) { + body.toplevel_page_automatic-ai-posts #wpbody-content { + display: flex; + flex-direction: column; + float: none; + height: calc(100vh - 32px - var(--aap-footer-h, 48px)); + min-height: 0; + padding: 0; + overflow: hidden; + box-sizing: border-box; + } + + body.toplevel_page_automatic-ai-posts #wpbody-content > .wrap { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + margin: 0; + padding: 0; + } + + #aap-app { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + } + + #aap-app .aap-shell { + flex: 1 1 auto; + min-height: 0; + align-items: stretch; + } + + #aap-app .aap-main { + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; + } + + #aap-app .aap-panel { + flex: 1 1 auto; + min-height: 0; + padding: 0; + display: flex; + flex-direction: column; + } + + #aap-app .aap-panel-body { + flex: 1 1 auto; + min-height: 0; + padding: 16px 20px 22px; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + } + + /* Cabeçalho: item fixo no topo do painel, com divisa clara */ + #aap-app .aap-sticky-head { + position: static; + flex: 0 0 auto; + margin: 0; + padding: 18px 20px 6px; + box-shadow: 0 6px 12px -8px rgba(15, 23, 42, .22); + z-index: 2; + } + + #aap-app .aap-sticky-head::after { opacity: 1; } + + /* A sidebar preenche a coluna (a página não rola) */ + #aap-app .aap-sidebar-inner { + position: static !important; + top: auto !important; + left: auto !important; + width: auto !important; + height: 100% !important; + } +} + +@media (max-width: 1280px) { + #aap-app .aap-check-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} + +@media (max-width: 1100px) { + #aap-app .aap-shell { grid-template-columns: 200px minmax(0, 1fr); } + #aap-app .aap-cards { grid-template-columns: minmax(0, 1fr); } + #aap-app .aap-check-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } +} + +@media (max-width: 782px) { + #aap-app { margin: 0; } + #aap-app .aap-shell { grid-template-columns: minmax(0, 1fr); } + #aap-app .aap-sidebar { + border-radius: 0; + } + #aap-app .aap-sidebar-inner { + position: static; + height: auto; + max-height: none; + padding: 14px; + overflow: visible; + } + #aap-app .aap-brand { padding-bottom: 12px; } + #aap-app .aap-nav { flex-direction: row; flex-wrap: wrap; } + #aap-app .aap-nav-item { flex: 1 1 auto; padding: 9px 12px; font-size: 12.5px; } + #aap-app .aap-panel { + padding: 16px; + border-left: 1px solid var(--aap-border); + border-top: 0; + border-radius: 0; + min-height: 0; + } + #aap-app .aap-sticky-head { margin: -16px -16px 0; padding: 16px 16px 6px; border-radius: 0; } + #aap-app .aap-cat-links { gap: 6px; } + #aap-app .aap-field { grid-template-columns: minmax(0, 1fr); } + #aap-app .aap-field > label, + #aap-app .aap-field > .aap-label { padding-top: 0; } + #aap-app .aap-check-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + #aap-app .aap-actions { width: 100%; margin-left: 0; margin-top: 8px; } + #aap-app .aap-panel-head { flex-wrap: wrap; } +} + +/* ─── Aba Ajuda (contribua / créditos / sobre) ──────────────────────────── */ + +#aap-app .aap-help-stack { + display: flex; + flex-direction: column; + gap: 18px; +} + +#aap-app .aap-help-card { + background: #fff; + border: 1px solid var(--aap-border); + border-radius: 12px; + padding: 20px 22px; + box-shadow: 0 1px 2px rgba(15, 23, 42, .04); +} + +#aap-app .aap-help-card-head { + display: flex; + align-items: flex-start; + gap: 14px; + margin-bottom: 16px; +} + +#aap-app .aap-help-card-text h3 { + margin: 0; + font-size: 18px; + font-weight: 700; + color: var(--aap-text); + line-height: 1.25; +} + +#aap-app .aap-help-card-text p { + margin: 4px 0 0; + font-size: 13.5px; + color: var(--aap-muted); +} + +#aap-app .aap-help-ico { + display: grid; + place-items: center; + width: 46px; + height: 46px; + flex: 0 0 46px; + border-radius: 50%; + background: #eef2ff; + color: #2563eb; +} + +#aap-app .aap-help-ico svg { width: 22px; height: 22px; } + +#aap-app .aap-help-ico.is-red { background: #fee2e2; color: #dc2626; } +#aap-app .aap-help-ico.is-violet { background: #ede9fe; color: #7c3aed; } +#aap-app .aap-help-ico.is-amber { background: #fef3c7; color: #d97706; } +#aap-app .aap-help-ico.is-blue { background: #dbeafe; color: #2563eb; } +#aap-app .aap-help-ico.sm { width: 38px; height: 38px; flex-basis: 38px; } +#aap-app .aap-help-ico.sm svg { width: 19px; height: 19px; } + +/* Doação */ +#aap-app .aap-donate-box { + display: flex; + align-items: center; + gap: 22px; + padding: 20px; + background: #eef2ff; + border: 1px solid #e0e7ff; + border-radius: 12px; +} + +#aap-app .aap-donate-text { + flex: 1 1 auto; + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +#aap-app .aap-donate-head { + display: flex; + align-items: center; + gap: 10px; +} + +#aap-app .aap-donate-text h4 { + margin: 0; + font-size: 17px; + font-weight: 700; + color: var(--aap-text); +} + +#aap-app .aap-donate-text p { + margin: 0; + font-size: 13.5px; + color: #475569; + line-height: 1.55; +} + +#aap-app .aap-pix-btn { margin-top: 6px; } + +#aap-app .aap-donate-qr { + flex: 0 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +#aap-app .aap-donate-qr img { + display: block; + width: 150px; + height: 150px; + padding: 8px; + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(15, 23, 42, .08); +} + +#aap-app .aap-donate-qr > span { + font-size: 11.5px; + color: var(--aap-muted); +} + +#aap-app .aap-qr-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + width: 150px; + height: 150px; + padding: 12px; + border: 1px dashed #c7d2fe; + border-radius: 10px; + color: #6366f1; + text-align: center; + font-size: 11.5px; + line-height: 1.4; +} + +#aap-app .aap-qr-empty svg { width: 24px; height: 24px; } + +/* "ou" */ +/* Créditos */ +#aap-app .aap-credit-row { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 0; + border-top: 1px solid var(--aap-border-soft); +} + +#aap-app .aap-credit-row:first-of-type { border-top: 0; padding-top: 4px; } + +#aap-app .aap-credit-avatar { + display: grid; + place-items: center; + width: 48px; + height: 48px; + flex: 0 0 48px; + border-radius: 12px; + background: #eef2ff; + color: #2563eb; +} + +#aap-app .aap-credit-avatar svg { width: 26px; height: 26px; } +#aap-app .aap-credit-avatar.is-blue { background: #dbeafe; color: #1d4ed8; } + +#aap-app .aap-credit-text { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 13.5px; + color: var(--aap-muted); +} + +#aap-app .aap-credit-text strong { + color: var(--aap-text); + font-size: 14.5px; +} + +#aap-app .aap-credit-name { font-weight: 600; color: #334155; } + +#aap-app .aap-credit-text a { + color: #2563eb; + text-decoration: none; + font-size: 13px; +} + +#aap-app .aap-credit-text a:hover { text-decoration: underline; } + +/* Sobre */ +#aap-app .aap-about-box { + display: flex; + align-items: center; + gap: 14px; + padding: 16px; + background: #eff6ff; + border: 1px solid #dbeafe; + border-radius: 12px; +} + +#aap-app .aap-about-icon { + display: grid; + place-items: center; + width: 54px; + height: 54px; + flex: 0 0 54px; + border-radius: 50%; + background: #dbeafe; + color: #1d4ed8; +} + +#aap-app .aap-about-icon svg { width: 30px; height: 30px; } + +#aap-app .aap-about-text { flex: 1 1 auto; min-width: 0; } + +#aap-app .aap-about-text h4 { + margin: 0 0 4px; + font-size: 17px; + font-weight: 700; + color: var(--aap-text); +} + +#aap-app .aap-about-text p { + margin: 0; + font-size: 13px; + color: #475569; + line-height: 1.5; +} + +#aap-app .aap-version-pill { + flex: 0 0 auto; + align-self: flex-start; + padding: 6px 12px; + border-radius: 99px; + background: #dbeafe; + color: #1e40af; + font-size: 12.5px; + font-weight: 700; + white-space: nowrap; +} + +#aap-app .aap-about-features { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 24px; + margin: 16px 0 0; + padding: 0; + list-style: none; +} + +#aap-app .aap-about-features li { + display: flex; + align-items: center; + gap: 9px; + font-size: 13.5px; + color: #334155; +} + +#aap-app .aap-check-dot { + display: grid; + place-items: center; + width: 18px; + height: 18px; + flex: 0 0 18px; + border-radius: 50%; + background: #2563eb; + color: #fff; +} + +#aap-app .aap-check-dot svg { width: 11px; height: 11px; stroke-width: 2.6; } + +#aap-app .aap-help-links { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid var(--aap-border-soft); + flex-wrap: wrap; +} + +#aap-app .aap-help-links a { + display: inline-flex; + align-items: center; + gap: 6px; + color: #2563eb; + font-size: 13.5px; + font-weight: 500; + text-decoration: none; +} + +#aap-app .aap-help-links a:hover { text-decoration: underline; } +#aap-app .aap-help-links svg { width: 16px; height: 16px; } + +@media (max-width: 782px) { + #aap-app .aap-donate-box { flex-direction: column; align-items: stretch; } + #aap-app .aap-donate-qr { align-self: center; } + #aap-app .aap-about-features { grid-template-columns: minmax(0, 1fr); } + #aap-app .aap-about-box { flex-wrap: wrap; } + #aap-app .aap-help-links { flex-direction: column; align-items: flex-start; } +} + +#aap-app .aap-pix-key { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 4px; + font-size: 12.5px; + color: #475569; +} + +#aap-app .aap-pix-key code { + padding: 3px 8px; + border-radius: 6px; + background: #fff; + border: 1px solid #dbeafe; + color: #1e40af; + font-size: 12px; + word-break: break-all; +} diff --git a/wp-content/plugins/automatic-ai-posts/admin/admin.js b/wp-content/plugins/automatic-ai-posts/admin/admin.js new file mode 100644 index 0000000..11c2b95 --- /dev/null +++ b/wp-content/plugins/automatic-ai-posts/admin/admin.js @@ -0,0 +1,802 @@ +jQuery(function ($) { + + if (typeof aapData === 'undefined') return; + + var ajaxurl = aapData.ajaxurl; + var s = aapData.s; // translated strings + + var $doc = $(document); + var $form = $('#aap-settings-form'); + var $exec = $('#rap-auto-interval'); + + // O