Ir para Conteúdo Principal

Como este hub foi feito

  • Hub

    Tema escuro e claro sobre as variáveis do UT

    O pf.css aponta as variáveis --ut-* do Universal Theme para tokens próprios; o tema claro redefine esses tokens no html só quando o style Iris está ativo. O botão Tema troca o style da sessão com apex_theme.set_session_style, guarda a escolha no navegador e, sem escolha salva, segue o tema do aparelho.

      /* o tema passa a ler os tokens; o Vita - Dark nao traz a Oracle Sans */
      --a-base-font-family: "Oracle Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
      --ut-palette-primary: var(--pf-azul);
      --ut-palette-primary-contrast: var(--pf-azul-tinta);
      --ut-palette-primary-shade: color-mix(in srgb, var(--pf-iris) 22%, transparent);
      --ut-link-text-color: var(--pf-azul);
      --ut-focus-outline-color: var(--pf-azul);
      --ut-body-text-color: var(--pf-texto);
      --ut-body-background-color: var(--pf-fundo);
      --ut-component-background-color: var(--pf-superficie);
      --ut-component-text-title-color: var(--pf-texto);
      --ut-component-text-default-color: var(--pf-texto-2);
      --ut-component-text-muted-color: var(--pf-texto-3);
      --ut-component-border-color: var(--pf-linha);
  • Início

    Entrada animada só na primeira página

    Um script no head marca a página antes da primeira pintura, só na primeira página da visita ou no F5, e o CSS anima topo, foto e seções em cerca de 1 s; a troca de página usa view transition entre documentos. Todo o movimento fica dentro de prefers-reduced-motion: no-preference.

    (function () {
      "use strict";
      try {
        var navegacao = performance.getEntriesByType ? performance.getEntriesByType("navigation")[0] : null,
            interna = sessionStorage.getItem("pf-recarga-interna");
        sessionStorage.removeItem("pf-recarga-interna");
        if (!sessionStorage.getItem("pf-entrou") || (navegacao && navegacao.type === "reload" && !interna)) {
          sessionStorage.setItem("pf-entrou", "1");
          document.documentElement.classList.add("pf-Entrada");
        }
      } catch (e) {
        // sem sessionStorage (navegacao restrita) a pagina abre sem animacao
      }
    })();
  • Hub

    Três idiomas no mesmo app

    Português, inglês e espanhol num app só, sem cópia traduzida: a interface usa Text Messages e o conteúdo das tabelas passa por apex_lang.lang, com Dynamic Translations. Na criação da sessão, um processo lê o Accept-Language e a primeira página já sai no idioma do navegador, se for um dos três; senão, em português.

      if owa.num_cgi_vars > 0 then
        -- o ORDS do ADB repassa os cabecalhos com o nome cru, em minusculas
        l_aceita := lower(nvl(owa_util.get_cgi_env('accept-language'), owa_util.get_cgi_env('HTTP_ACCEPT_LANGUAGE')));
      end if;
      for r in (select column_value tag from table(apex_string.split(l_aceita, ','))) loop
        l_tag := substr(trim(regexp_substr(r.tag, '^[^;]+')), 1, 2);
        if l_tag in ('pt', 'en', 'es') then
          l_idioma := case l_tag when 'pt' then 'pt-br' else l_tag end;
          exit;
        end if;
      end loop;
      apex_util.set_session_lang(p_lang => l_idioma);
      -- o set_session_lang so vale da proxima requisicao em diante; sem isto a primeira pagina sairia em portugues
      apex_application.g_browser_language := l_idioma;
  • Início

    Template Component próprio: Card de Sistema

    Os cards da página Início vêm de um plug-in Template Component publicado em repositório aberto: uma consulta SQL vira cards em carrossel, com seleção que dispara um evento na região e links limitados a http, https ou relativo. Ícone e selo reaproveitam o Avatar e o Badge do tema com {apply}.

    {if APEX$IS_LAZY_LOADING/}
      <div class="pf-Card pf-Card--carregando"></div>
    {else/}
      <article class="pf-Card pf-Card--{case COR/}{when success/}success{when info/}info{when warning/}warning{when danger/}danger{when generic/}generic{otherwise/}primary{endcase/}{if SELECIONADO/} is-selecionado{endif/}" data-chave="#CHAVE#">
        <div class="pf-Card-topo">
          {with/}
            TYPE:=icon
            ICON:=fa #ICONE#
            SHAPE:=t-Avatar--rounded
            SIZE:=t-Avatar--md
          {apply THEME$AVATAR/}
  • Hub

    Lançador aberto por ação nativa

    O ícone de grade na barra abre o lançador em qualquer página: a entrada da Navigation Bar aponta para #action$pf-lancador, ação do apex.actions que abre uma região da página global. É o mesmo mecanismo da entrada Instalar do PWA, e a lista vem de SQL: sistema publicado aparece sozinho.

      // a entrada da barra aponta para #action$pf-lancador, o mesmo mecanismo do "Install App";
      // apex.actions so fica pronto depois da carga dos arquivos da pagina
      apex.jQuery(function () {
        apex.actions.add([{
          name: "pf-lancador",
          label: texto("PF_LANCADOR"),
          action: function () {
            apex.theme.openRegion("lancador");
          }
        }]);
      });
  • Sistema

    Capa animada com imagem parada

    Nas páginas do Tooltip, do IG Quick Switch e do Card de Sistema, a demonstração é um WebP animado em <picture>, com <source> em JPG para prefers-reduced-motion: reduce; o navegador escolhe, sem JavaScript. O arquivo estático tem cache permanente, então o tamanho dele entra na URL como versão.

        -- capa cadastrada sem o arquivo no app: melhor sem demonstracao do que com imagem quebrada
        if l_v_capa is not null then
          :P2_DEMO_HTML := '<a class="pf-Hero-demo" href="' || apex_escape.html_attribute(l_raiz || l_capa || '?v=' || l_v_capa)
            || '" target="_blank" rel="noopener"><picture>'
            || case when l_v_parada is not null then '<source srcset="'
                 || apex_escape.html_attribute(l_raiz || l_parada || '?v=' || l_v_parada)
                 || '" media="(prefers-reduced-motion: reduce)">' end
            || '<img src="' || apex_escape.html_attribute(l_raiz || l_capa || '?v=' || l_v_capa) || '" alt="'
            || apex_escape.html_attribute(apex_lang.message('PF_DEMONSTRACAO') || ': ' || :P2_NOME) || '" decoding="async">'
            || '</picture></a>';
  • Hub

    CSP estrita com nonce da 26.1

    Sem unsafe-inline: script e estilo inline só rodam com o nonce que o APEX 26.1 gera a cada resposta (#APEX_CSP_NONCE#), e arquivo só do próprio site; por isso o hub não tem style nem onclick inline. Somam-se frame-ancestors 'none', X-Frame-Options DENY, cache desligado e Permissions-Policy sem GPS, câmera e microfone.

    Content-Security-Policy: default-src 'self' #APEX_CSP_NONCE#; object-src 'none'; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
    Permissions-Policy: geolocation=(), camera=(), microphone=()
  • Hub

    Cookie __Host- e bloqueio de origem externa

    Todo Autonomous Database da região conta como o mesmo site, e o ORDS devolve CORS com credenciais a qualquer origem. O cookie de sessão tem prefixo __Host- (Secure, Path /, sem Domain), que outro subdomínio não sobrescreve, e o Initialization PL/SQL Code recusa Origin diferente do próprio host.

    declare
      l_origem varchar2(4000);
    begin
      -- o ORDS responde CORS para qualquer origem, com credenciais, e todo ADB da regiao conta como o mesmo site:
      -- a pagina de outro cliente leria esta com o cookie do visitante. Navegacao comum nao manda Origin e as
      -- chamadas da propria pagina mandam este host. Fora de requisicao HTTP (job, SQLcl) nao ha CGI e nada a conferir
      if owa.num_cgi_vars > 0 then
        -- o ORDS do ADB repassa o cabecalho com o nome cru (origin), nao como HTTP_ORIGIN
        l_origem := lower(nvl(owa_util.get_cgi_env('origin'), owa_util.get_cgi_env('HTTP_ORIGIN')));
        if l_origem is not null and l_origem <> 'https://' || lower(owa_util.get_cgi_env('HTTP_HOST')) then
          raise_application_error(-20900, 'Origem nao permitida.');
        end if;
      end if;
    end;
  • Hub

    Links de saída por rota de redirecionamento

    LinkedIn, GitHub, currículo, demos, repositórios e APIs saem pela página 9, que recebe só um código (P9_DESTINO, P9_SISTEMA) e busca a URL no banco, no perfil ou em sistema publicado. Código desconhecido volta à Início, e a rota não vira redirecionamento aberto; o clique entra no contador diário, sem IP.

        when 'REPO' then
          select url_repo into l_url from pf_sistemas where alias = l_sistema and publicado = 'S';
          l_acao := 'CLICK_REPO';
        when 'API' then
          select url_api into l_url from pf_sistemas where alias = l_sistema and publicado = 'S';
          l_acao := 'CLICK_API';
        else
          l_url := null;
      end case;
    
      if l_url is null then
        apex_util.redirect_url(apex_page.get_url(p_page => 1));
      end if;
      pkg_pf_acessos.contar(p_pagina => 'ir', p_acao => l_acao, p_sistema_alias => l_sistema);
  • Privacidade

    Contador de visitas sem dado pessoal

    Cada visita ou clique soma 1 numa linha por dia, página, ação, sistema e tipo de aparelho: um MERGE só incrementa o total, sem gravar IP, sessão ou User-Agent. Os valores aceitos ficam nas constraints CHECK da tabela; valor fora da lista gera ORA-02290, que o package descarta sem gravar nem avisar.

           on (    d.dia = s.dia
               and d.pagina_alias = s.pagina_alias
               and d.acao = s.acao
               and d.sistema_id = s.sistema_id
               and d.dispositivo = s.dispositivo)
         when matched then
           update set d.total = d.total + 1
         when not matched then
           insert (dia, pagina_alias, acao, sistema_id, dispositivo, total)
           values (s.dia, s.pagina_alias, s.acao, s.sistema_id, s.dispositivo, 1);
      exception
        when e_fora_do_dominio then
          null; -- valor inventado por quem chamou: nao conta e nao avisa
      end contar;