Initial pub.solar commit
This commit is contained in:
parent
cbed1f320b
commit
234a648149
114
flake.lock
Normal file
114
flake.lock
Normal file
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"nodes": {
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": ["nixpkgs"]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705332421,
|
||||
"narHash": "sha256-USpGLPme1IuqG78JNqSaRabilwkCyHmVWY0M9vYyqEA=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "83cb93d6d063ad290beee669f4badf9914cc16ec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1707092692,
|
||||
"narHash": "sha256-ZbHsm+mGk/izkWtT4xwwqz38fdlwu7nUUKXTOmm4SyE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "faf912b086576fd1a15fca610166c98d47bc667e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
54
flake.nix
Normal file
54
flake.nix
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
description = "Keycloak pub.solar theme";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
devshell.url = "github:numtide/devshell";
|
||||
devshell.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, flake-utils, devshell, nixpkgs }:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
overlays = [
|
||||
devshell.overlay
|
||||
(import ./overlay.nix)
|
||||
];
|
||||
};
|
||||
in
|
||||
flake-utils.lib.simpleFlake {
|
||||
inherit self nixpkgs;
|
||||
name = "keycloak-theme-pub-solar";
|
||||
overlay = ./overlay.nix;
|
||||
preOverlays = [ devshell.overlays.default ];
|
||||
shell = { pkgs }:
|
||||
let
|
||||
google-font-downloader = with pkgs; writeShellScriptBin "google-font-downloader" ''
|
||||
echo "Attempting to fetch $1"
|
||||
${nodejs}/bin/npx google-font-downloader -- $1
|
||||
rm ./common/resources/scss/typography.scss
|
||||
echo "/* To regenerate this file, run $ google-font-downloader '$1' */" >> ./common/resources/scss/typography.scss
|
||||
echo "" >> ./common/resources/scss/typography.scss
|
||||
cat ./google-fonts-*.css >> ./common/resources/scss/typography.scss
|
||||
rm ./google-fonts-*.css
|
||||
mv fonts ./common/resources/fonts
|
||||
'';
|
||||
in
|
||||
pkgs.devshell.mkShell {
|
||||
imports = [ ];
|
||||
|
||||
# Add additional packages you'd like to be available in your devshell
|
||||
# PATH here
|
||||
devshell.packages = [
|
||||
google-font-downloader
|
||||
pkgs.nodejs
|
||||
pkgs.nodePackages.typescript
|
||||
pkgs.nodePackages.typescript-language-server
|
||||
pkgs.nodePackages.vue-language-server
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
22
overlay.nix
Normal file
22
overlay.nix
Normal file
|
@ -0,0 +1,22 @@
|
|||
final: prev:
|
||||
let
|
||||
pkgs = final;
|
||||
version = "0.1";
|
||||
keycloak-account-v1 = prev.fetchMavenArtifact {
|
||||
artifactId = "keycloak-account-v1";
|
||||
groupId = "io.phasetwo.keycloak";
|
||||
inherit version;
|
||||
hash = "sha256-t4kuc5ZieqsC06/alFN0W1ktORuk36TIgKXfrmBtesA=";
|
||||
};
|
||||
in
|
||||
{
|
||||
# this key should be the same as the simpleFlake name attribute.
|
||||
keycloak-theme-pub-solar = {
|
||||
keycloak-theme-pub-solar = import ./pkgs/keycloak-theme-pub-solar.nix { inherit pkgs; };
|
||||
keycloak = prev.keycloak.overrideAttrs (finalAttrs: previousAttrs: {
|
||||
installPhase = previousAttrs.installPhase + ''
|
||||
ln -s ${keycloak-account-v1}/share/java/keycloak-account-v1-${version}.jar $out/providers/keycloak-account-v1-${version}.jar
|
||||
'';
|
||||
});
|
||||
};
|
||||
}
|
|
@ -22,7 +22,6 @@
|
|||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"storybook": "^8.1.10",
|
||||
"@storybook/react": "^8.1.10",
|
||||
"@storybook/react-vite": "^8.1.10",
|
||||
"@types/react": "^18.2.43",
|
||||
|
@ -35,6 +34,8 @@
|
|||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"prettier": "3.3.1",
|
||||
"sass": "^1.80.7",
|
||||
"storybook": "^8.1.10",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
},
|
||||
|
|
119
public/pub.solar.svg
Normal file
119
public/pub.solar.svg
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="Layer_3"
|
||||
data-name="Layer 3"
|
||||
viewBox="0 0 275.3 276.37"
|
||||
version="1.1"
|
||||
sodipodi:docname="pub.solar.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<sodipodi:namedview
|
||||
id="namedview226"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.3962803"
|
||||
inkscape:cx="95.762774"
|
||||
inkscape:cy="149.33079"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1380"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_3" />
|
||||
<defs
|
||||
id="defs197">
|
||||
<style
|
||||
id="style195">.cls-1,.cls-2{fill:#ed1c24;}.cls-1{stroke:#ed1c24;stroke-width:6.89px;}.cls-1,.cls-2,.cls-4{stroke-miterlimit:10;}.cls-2{stroke:#fff;stroke-width:4.72px;}.cls-3{fill:#fff;}.cls-4{stroke:#000;stroke-width:4.58px;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title199">PubSolar logo</title>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M362.85,272.68v11.78l-4.39,1.84a18.38,18.38,0,0,0-11.24,15.8l-.59,9.43-7.92.61a18.38,18.38,0,0,0-15.53,11.22l-2.47,5.9H309.07a18.38,18.38,0,0,0-14.13,6.63l-6.8,8.17-10.58-3.17a18.38,18.38,0,0,0-14.89,1.95l-9.84,6-8.65-5.75a18.38,18.38,0,0,0-15.75-2.2l-10,3.17-5.88-7a18.38,18.38,0,0,0-15.43-6.51l-10.24.76-4.62-8.75a18.38,18.38,0,0,0-14.22-9.69l-9.7-1.08-1.89-10.65a18.38,18.38,0,0,0-11.06-13.77L134.7,283l.93-11.32a18.38,18.38,0,0,0-7-16l-6.52-5.06,3.55-8.77a18.38,18.38,0,0,0-1.77-17.13l-5.74-8.57,6.06-8a18.38,18.38,0,0,0,2.54-17.63l-3.93-10.34,5.12-2.87a18.38,18.38,0,0,0,9.31-17.78l-1.12-11.75,8.18-2.64a18.38,18.38,0,0,0,12.67-16l.75-9.5h6.44a18.38,18.38,0,0,0,17.59-13L184,99.13l12.19,1.25a18.38,18.38,0,0,0,17.27-8.25l4.11-6.3,8.21,3.79a18.38,18.38,0,0,0,20.12-3.14l5.85-5.36L261,88.21a18.38,18.38,0,0,0,18.23,2.32L287.76,87l6,7.67a18.38,18.38,0,0,0,15.32,7l11.25-.51,3.21,7.56a18.38,18.38,0,0,0,15.29,11.12l9.33.83.48,7.39A18.38,18.38,0,0,0,358.1,143l9.37,5.18-.71,11.41a18.38,18.38,0,0,0,7.43,15.93l7.14,5.26-3.79,10.66a18.38,18.38,0,0,0,2,16.27l5.57,8.45-6.46,10.31a18.38,18.38,0,0,0-2.11,14.77l2.66,9.38-9.63,7.93A18.38,18.38,0,0,0,362.85,272.68Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path201" />
|
||||
<circle
|
||||
class="cls-2"
|
||||
cx="137.72"
|
||||
cy="138.48"
|
||||
r="117.79"
|
||||
id="circle203" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="M326.34,141.78A105.72,105.72,0,0,0,181.56,295.61,105.7,105.7,0,1,1,326.34,141.78Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path205" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 180.73,231.12 c 0.57,1.71 6.47,20.7 10,30 3.53,9.3 7.59,16.9 12.87,17.74 5.28,0.84 10.64,-7.56 16.9,-7.78 8.76,-0.31 13.68,-1 18.13,-8.12 3.56,-5.67 9.9,1.76 25.87,-7.49 8.71,-5 26.56,-4.08 43.4,-11.91 20.26,-9.42 6.82,-12 6.83,-24 0,-3.84 3.66,-4.88 9.42,-7.17 3.82,-1.52 9.75,-4.8 8.63,-8.92 -1.24,-4.55 -0.79,-6.28 -5.2,-7.93 -8.95912,-3.35701 -15.41112,0.51436 -20.81894,2.56685 -7.90305,3.13369 -9.25397,-5.78637 -14.68734,-7.38637 -3.84252,-1.13153 -10.81538,-2.53029 -12.21968,-9.01195 C 278.1258,173.18465 270.1,170.58 261.8,170.42 c -4.33,-0.08 -8,1.37 -11,-5.82 -3.38,-8.07 -10.07,-19.35 -16.92,-12 -6.85,7.35 2,17.55 3.13,27 0.19,1.55 -0.08,3 -1.63,3.23 -3.79619,0.48553 -7.30604,2.27403 -9.93,5.06 -4.27,4.41 -8.18,-2.59 -15.09,3.8 -6.32,5.85 -13.27,-0.73 -25.52,-0.41 -20.65,0.56 -4.11,39.84 -4.11,39.84 z"
|
||||
transform="translate(-113.88,-76.62)"
|
||||
id="path207"
|
||||
sodipodi:nodetypes="csscccccccccscccscccccc" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M200.74,254.41a27,27,0,0,0,1.31,3c2.16,4.33,1.11,2.86,3.11,6.36,1.22,2.13,4.06,6.21,2.11,6.86-1.68.56-4.06-1-4.75-3.08-.34-1-.59-2.08-.93-3.1-2.72-8.19-6.6-15.8-9.33-24-3-9-5.76-18.25-7.12-27.94-.46-3.57-3.28-10.18-.86-11,1.49-.49,2.14.53,2.48,1.55.74,2.23.09,2.45.5,3.66a1.31,1.31,0,0,0,1.7.78c1.68-.56,1.9-3.63,3.95-4.31,7.36-2.45,15.23,5.38,17.4,11.9,1.48,6-1.4,10.39-6.34,15.14a7.39,7.39,0,0,1-2.86,1.67c-2,.68-4.81.46-6.12.89s-1.61,1.36-1.15,2.76,1.21,2.7,1.67,4.1c.68,2,1.15,4.07,1.83,6.11a13.67,13.67,0,0,0,.71,1.83C198.94,249.94,200,252.18,200.74,254.41Zm-6.45-48.32c-6.52,2.17-4.88,13.31-3,19.08,1.3,3.91,4.19,5.44,8.09,4.14,5.59-1.86,9.23-8.65,7.37-14.24C205.11,211,199.22,204.45,194.29,206.09Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path209" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M216.34,197.41a.88.88,0,0,1,1.35.45c2.1,3.85,3.09,9.07,5.33,13.18,2.1,3.85,7.1,7,9.45,5.68,2.67-1.46,4.77-8.79,2.56-12.83-.64-1.17-1.64-2.24-2.5-3.8-1.32-2.41-2.15-6.11.13-7.35,1.37-.75,1.92-.2,2.31.52.07.13.08.29.15.43.25.46.59.78.84,1.23,1.1,2,1.14,4.88,1.54,6.86a21.85,21.85,0,0,0,2,6.08,14.16,14.16,0,0,0,4.19,4.74c.38.38,1,.73,1.24,1.18a1.24,1.24,0,0,1-.71,1.66c-.85.46-3.88-1.52-4.84-3.28-.21-.39-.3-.85-.55-1.31a.78.78,0,0,0-1.14-.23c-.65.36-.62,2.12-.83,2.82-.76,2-1.51,4.21-3.33,5.21-4,2.21-9.11-1.63-10.89-4.89-.43-.78-.79-1.6-1.25-2.45C219.43,207.58,215.43,197.91,216.34,197.41Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path211" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M251.1,184.25c.22.55.06,1.41,1.17,1,.62-.24.83-1.12,1.06-1.77a7.57,7.57,0,0,1,4.54-4.5c6.36-2.5,13.65,1.82,15.88,7.49,2.12,5.39,1,13.9-5.72,16.54a10.23,10.23,0,0,1-5.58.43,4,4,0,0,0-2-.18c-.9.35,0,2.33-1.08,2.74-1.94.76-3.25-.83-4.09-2.74-1.37-3.29-3.11-8.25-4.44-11.64s-2.43-7.19-3.84-10.79c-1.33-3.39-3-6.71-4.39-10.17-.6-1.52-1.13-3.07-1.7-4.52a17.75,17.75,0,0,0-2.25-4.31,1,1,0,0,1-.23-.39,1.46,1.46,0,0,1,1.06-1.77,1.54,1.54,0,0,1,2.14,1l.14.35a75.7,75.7,0,0,0,3.53,10.19c.35.9.8,1.84,1.15,2.74l-.07,0C247.73,177.34,249.77,180.86,251.1,184.25Zm8-3.06c-5.33,2.09-6.44,8.2-4.32,13.59a9.89,9.89,0,0,0,13,5.9c4.36-1.71,5.62-9.07,3.88-13.5C268.24,181.58,263.7,179.37,259.07,181.19Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path213" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M216.42,242.8c-.49-2.48,2.88-6.17,6.16-6.81,1.83-.36,4.94.47,5.28,2.22a1.91,1.91,0,0,1-1.12,2c-1.38.27-2.88-1.93-4.33-1.65s-3.39,2.41-3.13,3.72c.59,3,7.19,5.79,10.06,8.48a7,7,0,0,1,2.07,3.61,7.5,7.5,0,0,1-5.63,8.37c-5.76,1.13-9.07-3-11.63-6.81a2,2,0,0,1-.39-.83,1.4,1.4,0,0,1,1-1.71c.87-.17,1.43.7,1.86,1.38,1.5,3,4,6.41,8.28,5.57,2.7-.53,4.44-3.22,3.79-6.57C227.21,249.93,217.23,247,216.42,242.8Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path215" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M244.38,250.12c-6.18-5.9-6.33-14.67-2.63-18.54,2.46-2.58,9.1-4.36,13.13-.51,3.39,3.23,6.36,13.57,1.38,18.78a6.15,6.15,0,0,1-5.21,1.91,7.1,7.1,0,0,0-3.3.34c-.83.33-1,.34-1.85-.43-.27-.26-.59-.67-1.08-1.13Zm8.85-17.44a6.87,6.87,0,0,0-9.3.16c-2.62,2.74-1.09,12.22,1.38,14.58s7.16,2.94,9.68.3C258,244.55,257.58,236.83,253.23,232.67Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path217" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M269.24,237.75a63.21,63.21,0,0,0-2.83-6.39s-1-2.71-1.6-4.88c-.76-1.94-1.59-3.84-2.35-5.78-2.09-5.33-10.17-21.45-6.49-20.18,1.83.63,2.79.82,3.06,1.52a28.65,28.65,0,0,1,1,4.84c1.23,4.66,4.57,13.94,6.83,19.68.9,2.28,1.89,4.61,2.93,6.83,2,4.46,4.33,9.33,4.18,10.25C273.94,243.64,272.62,248.25,269.24,237.75Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path219" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M288.05,226.57c1-.37,1.51-1,1.29-2.58-.3-2.13-2.48-6.48-4.62-6.18-1.84.26-3.45,3.78-4.85,4-.59.08-1.23-.73-1.29-1.17-.26-1.84,4.38-4.66,5.92-4.88,5.22-.73,7.52,9.3,9.65,15.91.44,1.52,1,2.41,2.74,2.39.68.06,1.72,0,1.83.72A2.08,2.08,0,0,1,297,237c-1.25.17-2.61-1.51-3.64-1.37-2.06.29-3.5,3.94-6.3,4.33a4.31,4.31,0,0,1-4.94-3.66c0-.22,0-.45,0-.67C281.56,230.56,283.06,228.39,288.05,226.57ZM284.42,235c.21,1.47.62,2.84,2.54,2.57s5.26-4.26,5-6.32c-.33-2.36-2.1-2.71-3.91-2.76C285.44,228.81,284,231.64,284.42,235Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path221" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M306.07,208c2.38-.87,14.61-5.28,16.95-6.05.86-.23,1.94-.52,2.28.78.92,3.45-22.71,6-20.38,14.81,1.28,4.81,2.85,9.55,4,14.3.25.93.1,1.82-1,2.11-2.08.56-4.25-10.17-4.27-10.24C301.92,216.9,299.43,210.42,306.07,208Z"
|
||||
transform="translate(-113.88 -76.62)"
|
||||
id="path223" />
|
||||
<ellipse
|
||||
style="fill:#000000;fill-rule:evenodd;stroke-width:0.83809"
|
||||
id="path429"
|
||||
cx="172.17104"
|
||||
cy="122.1034"
|
||||
rx="4.5852861"
|
||||
ry="4.9624691" />
|
||||
<metadata
|
||||
id="metadata3053">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>PubSolar logo</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
|
@ -1,5 +1,5 @@
|
|||
// This file is auto-generated by the `update-kc-gen` command. Do not edit it manually.
|
||||
// Hash: 52e835881710cf6fd39c1589bb9282feccdcf06c9547d41c919576489f251d2f
|
||||
// Hash: a4bb051dacf961c9963f104bb9f9b28088f10d7beadd6cab6df0eb3f56500a51
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
|
@ -19,7 +19,7 @@ export const kcEnvNames: KcEnvName[] = [];
|
|||
|
||||
export const kcEnvDefaults: Record<KcEnvName, string> = {};
|
||||
|
||||
export type KcContext = import("./login/KcContext").KcContext;
|
||||
type KcContext = import("./login/KcContext").KcContext;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
22
src/login/Background.tsx
Normal file
22
src/login/Background.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
const x1312 = (new Array(400)).fill("0x1312").join(" ");
|
||||
|
||||
export default function Background() {
|
||||
return <div
|
||||
id="background"
|
||||
className="ps-background ps-main--background"
|
||||
>
|
||||
<div
|
||||
id="x1312"
|
||||
className="ps-background--1312"
|
||||
>{x1312}</div>
|
||||
{(new Array(Math.ceil(window.innerWidth / 100) * Math.ceil(window.innerHeight / 100)))
|
||||
.fill(null)
|
||||
.map((_, i) => <div key={i} className="ps-background--logo ps-logo">
|
||||
<img
|
||||
className="ps-logo--base"
|
||||
src={`${import.meta.env.BASE_URL}pub.solar.svg`}
|
||||
/>
|
||||
</div>)
|
||||
}
|
||||
</div>;
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import "../scss/index.scss";
|
||||
import { Suspense, lazy } from "react";
|
||||
import type { ClassKey } from "keycloakify/login";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import DefaultPage from "keycloakify/login/DefaultPage";
|
||||
import Template from "keycloakify/login/Template";
|
||||
import Template from "./Template";
|
||||
const UserProfileFormFields = lazy(
|
||||
() => import("keycloakify/login/UserProfileFormFields")
|
||||
);
|
||||
|
@ -26,7 +27,6 @@ export default function KcPage(props: { kcContext: KcContext }) {
|
|||
i18n={i18n}
|
||||
classes={classes}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
UserProfileFormFields={UserProfileFormFields}
|
||||
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||
/>
|
||||
|
@ -37,4 +37,36 @@ export default function KcPage(props: { kcContext: KcContext }) {
|
|||
);
|
||||
}
|
||||
|
||||
const classes = {} satisfies { [key in ClassKey]?: string };
|
||||
const classes = {
|
||||
kcBodyClass: "ps-main ps-main_full",
|
||||
kcContentClass: "ps-content",
|
||||
kcLoginClass: "ps-main--page ps-page",
|
||||
|
||||
kcHeaderClass: "ps-page--header ps-header",
|
||||
kcHeaderWrapperClass: "ps-header--wrapper",
|
||||
|
||||
kcFormCardClass: "ps-page--section ps-page--section_full",
|
||||
|
||||
kcFormClass: "ps-form",
|
||||
|
||||
kcFormHeaderClass: "ps-form--header",
|
||||
|
||||
kcFormGroupClass: "ps-form-group",
|
||||
kcFormButtonsClass: "ps-form-group--buttons",
|
||||
kcFormOptionsClass: "ps-form-group--options",
|
||||
kcFormOptionsWrapperClass: "ps-form-group--options-wrapper",
|
||||
|
||||
kcButtonClass: "ps-button",
|
||||
kcLabelClass: "ps-label",
|
||||
kcInputClass: "ps-input",
|
||||
kcInputGroup: "ps-input-group",
|
||||
kcFormPasswordVisibilityButtonClass: "ps-input-group--password-visibility-button",
|
||||
|
||||
kcLocaleClass: "ps-locale",
|
||||
kcLocaleMainClass: "ps-locale ps-header--i18n",
|
||||
kcLocaleListClass: "ps-locale--list",
|
||||
kcLocaleListItemClass: "ps-locale--list-item",
|
||||
kcLocaleItemClass: "ps-locale--item",
|
||||
kcLocaleWrapperClass: "ps-locale--wrapper",
|
||||
kcLocaleDropDownClass: "ps-locale--dropdown"
|
||||
} satisfies { [key in ClassKey]?: string };
|
||||
|
|
185
src/login/Template.tsx
Normal file
185
src/login/Template.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
import { useEffect } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { TemplateProps } from "keycloakify/login/TemplateProps";
|
||||
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import { useInitialize } from "keycloakify/login/Template.useInitialize";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import Background from "./Background";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const {
|
||||
displayInfo = false,
|
||||
displayMessage = true,
|
||||
displayRequiredFields = false,
|
||||
headerNode,
|
||||
socialProvidersNode = null,
|
||||
infoNode = null,
|
||||
documentTitle,
|
||||
bodyClassName,
|
||||
kcContext,
|
||||
i18n,
|
||||
doUseDefaultCss,
|
||||
classes,
|
||||
children
|
||||
} = props;
|
||||
|
||||
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
|
||||
|
||||
const { realm, auth, url, message, isAppInitiatedAction } = kcContext;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = documentTitle ?? msgStr("loginTitle", kcContext.realm.displayName);
|
||||
}, []);
|
||||
|
||||
useSetClassName({
|
||||
qualifiedName: "html",
|
||||
className: kcClsx("kcHtmlClass")
|
||||
});
|
||||
|
||||
useSetClassName({
|
||||
qualifiedName: "body",
|
||||
className: bodyClassName ?? kcClsx("kcBodyClass")
|
||||
});
|
||||
|
||||
const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
|
||||
|
||||
if (!isReadyToRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Background />
|
||||
<div className={kcClsx("kcLoginClass")}>
|
||||
<div id="kc-header" className={kcClsx("kcHeaderClass")}>
|
||||
<a href="https://pub.solar/" className="ps-homelink">pub.solar/</a>
|
||||
{enabledLanguages.length > 1 && (
|
||||
<div className={kcClsx("kcLocaleMainClass")} id="kc-locale">
|
||||
<div id="kc-locale-wrapper" className={kcClsx("kcLocaleWrapperClass")}>
|
||||
<div id="kc-locale-dropdown" className={clsx("menu-button-links", kcClsx("kcLocaleDropDownClass"))}>
|
||||
<button
|
||||
tabIndex={1}
|
||||
id="kc-current-locale-link"
|
||||
aria-label={msgStr("languages")}
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-controls="language-switch1"
|
||||
>
|
||||
{currentLanguage.label}
|
||||
</button>
|
||||
<ul
|
||||
role="menu"
|
||||
tabIndex={-1}
|
||||
aria-labelledby="kc-current-locale-link"
|
||||
aria-activedescendant=""
|
||||
id="language-switch1"
|
||||
className={kcClsx("kcLocaleListClass")}
|
||||
>
|
||||
{enabledLanguages.map(({ languageTag, label, href }, i) => (
|
||||
<li key={languageTag} className={kcClsx("kcLocaleListItemClass")} role="none">
|
||||
<a role="menuitem" id={`language-${i + 1}`} className={kcClsx("kcLocaleItemClass")} href={href}>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={kcClsx("kcFormCardClass")}>
|
||||
<header className={kcClsx("kcFormHeaderClass")}>
|
||||
{(() => {
|
||||
const node = !(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
<h1 id="kc-page-title" className="ps-page--title">{headerNode}</h1>
|
||||
) : (
|
||||
<div id="kc-username" className={kcClsx("kcFormGroupClass")}>
|
||||
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={kcClsx("kcResetFlowIcon")}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (displayRequiredFields) {
|
||||
return (
|
||||
<div className={kcClsx("kcContentWrapperClass")}>
|
||||
<div className={clsx(kcClsx("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">{node}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return node;
|
||||
})()}
|
||||
</header>
|
||||
<div id="kc-content" className="ps-page--section-contents ps-container">
|
||||
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
|
||||
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
|
||||
<div
|
||||
className={clsx(
|
||||
`alert-${message.type}`,
|
||||
kcClsx("kcAlertClass"),
|
||||
`pf-m-${message?.type === "error" ? "danger" : message.type}`
|
||||
)}
|
||||
>
|
||||
<div className="pf-c-alert__icon">
|
||||
{message.type === "success" && <span className={kcClsx("kcFeedbackSuccessIcon")}></span>}
|
||||
{message.type === "warning" && <span className={kcClsx("kcFeedbackWarningIcon")}></span>}
|
||||
{message.type === "error" && <span className={kcClsx("kcFeedbackErrorIcon")}></span>}
|
||||
{message.type === "info" && <span className={kcClsx("kcFeedbackInfoIcon")}></span>}
|
||||
</div>
|
||||
<span
|
||||
className={kcClsx("kcAlertTitleClass")}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(message.summary)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{auth !== undefined && auth.showTryAnotherWayLink && (
|
||||
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
||||
<div className={kcClsx("kcFormGroupClass")}>
|
||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||
<a
|
||||
href="#"
|
||||
id="try-another-way"
|
||||
onClick={() => {
|
||||
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{msg("doTryAnotherWay")}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{socialProvidersNode}
|
||||
{displayInfo && (
|
||||
<div id="kc-info" className={kcClsx("kcSignUpClass")}>
|
||||
<div id="kc-info-wrapper" className={kcClsx("kcInfoAreaWrapperClass")}>
|
||||
{infoNode}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
62
src/login/pages/Error.stories.tsx
Normal file
62
src/login/pages/Error.stories.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "error.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/error.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithAnotherMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: { summary: "With another error message" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithHtmlErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "<strong>Error:</strong> Something went wrong. <a href='https://example.com'>Go back</a>"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const FrenchError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
locale: { currentLanguageTag: "fr" },
|
||||
message: { summary: "Une erreur s'est produite" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithSkipLink: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: { summary: "An error occurred" },
|
||||
skipLink: true,
|
||||
client: {
|
||||
baseUrl: "https://example.com"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
95
src/login/pages/Info.stories.tsx
Normal file
95
src/login/pages/Info.stories.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "info.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/info.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "Server info message"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithLinkBack: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "Server message"
|
||||
},
|
||||
actionUri: undefined
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithRequiredActions: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "Required actions: "
|
||||
},
|
||||
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"],
|
||||
"x-keycloakify": {
|
||||
messages: {
|
||||
"requiredAction.CUSTOM_ACTION": "Custom action"
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithPageRedirect: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: { summary: "You will be redirected shortly." },
|
||||
pageRedirectUri: "https://example.com"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithoutClientBaseUrl: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: { summary: "No client base URL defined." },
|
||||
client: { baseUrl: undefined }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithMessageHeader: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
messageHeader: "Important Notice",
|
||||
message: { summary: "This is an important message." }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithAdvancedMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: { summary: "Please take note of this <strong>important</strong> information." }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
360
src/login/pages/Login.stories.tsx
Normal file
360
src/login/pages/Login.stories.tsx
Normal file
|
@ -0,0 +1,360 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithInvalidCredential: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
login: {
|
||||
username: "johndoe"
|
||||
},
|
||||
messagesPerField: {
|
||||
// NOTE: The other functions of messagesPerField are derived from get() and
|
||||
// existsError() so they are the only ones that need to mock.
|
||||
existsError: (fieldName: string, ...otherFieldNames: string[]) => {
|
||||
const fieldNames = [fieldName, ...otherFieldNames];
|
||||
return fieldNames.includes("username") || fieldNames.includes("password");
|
||||
},
|
||||
get: (fieldName: string) => {
|
||||
if (fieldName === "username" || fieldName === "password") {
|
||||
return "Invalid username or password.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutRegistration: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { registrationAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutRememberMe: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { rememberMe: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutPasswordReset: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { resetPasswordAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { loginWithEmailAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithPresetUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
login: { username: "max.mustermann@mail.com" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithImmutablePresetUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
auth: {
|
||||
attemptedUsername: "max.mustermann@mail.com",
|
||||
showUsername: true
|
||||
},
|
||||
usernameHidden: true,
|
||||
message: {
|
||||
type: "info",
|
||||
summary: "Please re-authenticate to continue"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithSocialProviders: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
},
|
||||
{
|
||||
loginUrl: "microsoft",
|
||||
alias: "microsoft",
|
||||
providerId: "microsoft",
|
||||
displayName: "Microsoft",
|
||||
iconClasses: "fa fa-windows"
|
||||
},
|
||||
{
|
||||
loginUrl: "facebook",
|
||||
alias: "facebook",
|
||||
providerId: "facebook",
|
||||
displayName: "Facebook",
|
||||
iconClasses: "fa fa-facebook"
|
||||
},
|
||||
{
|
||||
loginUrl: "instagram",
|
||||
alias: "instagram",
|
||||
providerId: "instagram",
|
||||
displayName: "Instagram",
|
||||
iconClasses: "fa fa-instagram"
|
||||
},
|
||||
{
|
||||
loginUrl: "twitter",
|
||||
alias: "twitter",
|
||||
providerId: "twitter",
|
||||
displayName: "Twitter",
|
||||
iconClasses: "fa fa-twitter"
|
||||
},
|
||||
{
|
||||
loginUrl: "linkedin",
|
||||
alias: "linkedin",
|
||||
providerId: "linkedin",
|
||||
displayName: "LinkedIn",
|
||||
iconClasses: "fa fa-linkedin"
|
||||
},
|
||||
{
|
||||
loginUrl: "stackoverflow",
|
||||
alias: "stackoverflow",
|
||||
providerId: "stackoverflow",
|
||||
displayName: "Stackoverflow",
|
||||
iconClasses: "fa fa-stack-overflow"
|
||||
},
|
||||
{
|
||||
loginUrl: "github",
|
||||
alias: "github",
|
||||
providerId: "github",
|
||||
displayName: "Github",
|
||||
iconClasses: "fa fa-github"
|
||||
},
|
||||
{
|
||||
loginUrl: "gitlab",
|
||||
alias: "gitlab",
|
||||
providerId: "gitlab",
|
||||
displayName: "Gitlab",
|
||||
iconClasses: "fa fa-gitlab"
|
||||
},
|
||||
{
|
||||
loginUrl: "bitbucket",
|
||||
alias: "bitbucket",
|
||||
providerId: "bitbucket",
|
||||
displayName: "Bitbucket",
|
||||
iconClasses: "fa fa-bitbucket"
|
||||
},
|
||||
{
|
||||
loginUrl: "paypal",
|
||||
alias: "paypal",
|
||||
providerId: "paypal",
|
||||
displayName: "PayPal",
|
||||
iconClasses: "fa fa-paypal"
|
||||
},
|
||||
{
|
||||
loginUrl: "openshift",
|
||||
alias: "openshift",
|
||||
providerId: "openshift",
|
||||
displayName: "OpenShift",
|
||||
iconClasses: "fa fa-cloud"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutPasswordField: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { password: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "The time allotted for the connection has elapsed.<br/>The login process will restart from the beginning.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithOneSocialProvider: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithTwoSocialProviders: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
},
|
||||
{
|
||||
loginUrl: "microsoft",
|
||||
alias: "microsoft",
|
||||
providerId: "microsoft",
|
||||
displayName: "Microsoft",
|
||||
iconClasses: "fa fa-windows"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithNoSocialProviders: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: []
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithMoreThanTwoSocialProviders: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
},
|
||||
{
|
||||
loginUrl: "microsoft",
|
||||
alias: "microsoft",
|
||||
providerId: "microsoft",
|
||||
displayName: "Microsoft",
|
||||
iconClasses: "fa fa-windows"
|
||||
},
|
||||
{
|
||||
loginUrl: "facebook",
|
||||
alias: "facebook",
|
||||
providerId: "facebook",
|
||||
displayName: "Facebook",
|
||||
iconClasses: "fa fa-facebook"
|
||||
},
|
||||
{
|
||||
loginUrl: "twitter",
|
||||
alias: "twitter",
|
||||
providerId: "twitter",
|
||||
displayName: "Twitter",
|
||||
iconClasses: "fa fa-twitter"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithSocialProvidersAndWithoutRememberMe: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
}
|
||||
]
|
||||
},
|
||||
realm: { rememberMe: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
127
src/login/pages/LoginOtp.stories.tsx
Normal file
127
src/login/pages/LoginOtp.stories.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-otp.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-otp.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* MultipleOtpCredentials:
|
||||
* - Purpose: Tests the behavior when the user has multiple OTP credentials to choose from.
|
||||
* - Scenario: Simulates the scenario where the user is presented with multiple OTP credentials and must select one to proceed.
|
||||
* - Key Aspect: Ensures that multiple OTP credentials are listed and selectable, and the correct credential is selected by default.
|
||||
*/
|
||||
export const MultipleOtpCredentials: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
otpLogin: {
|
||||
userOtpCredentials: [
|
||||
{ id: "credential1", userLabel: "Device 1" },
|
||||
{ id: "credential2", userLabel: "Device 2" },
|
||||
{ id: "credential2", userLabel: "Device 3" },
|
||||
{ id: "credential2", userLabel: "Device 4" },
|
||||
{ id: "credential2", userLabel: "Device 5" },
|
||||
{ id: "credential2", userLabel: "Device 6" }
|
||||
],
|
||||
selectedCredentialId: "credential1"
|
||||
},
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: () => false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithOtpError:
|
||||
* - Purpose: Tests the behavior when an error occurs with the OTP field (e.g., invalid OTP code).
|
||||
* - Scenario: Simulates an invalid OTP code scenario where an error message is displayed.
|
||||
* - Key Aspect: Ensures that the OTP input displays error messages correctly and the error is visible.
|
||||
*/
|
||||
export const WithOtpError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
otpLogin: {
|
||||
userOtpCredentials: []
|
||||
},
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (field: string) => field === "totp",
|
||||
get: () => "Invalid OTP code"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* NoOtpCredentials:
|
||||
* - Purpose: Tests the behavior when no OTP credentials are provided for the user.
|
||||
* - Scenario: Simulates the scenario where the user is not presented with any OTP credentials, and only the OTP input is displayed.
|
||||
* - Key Aspect: Ensures that the component handles cases where there are no user OTP credentials, and the user is only prompted for the OTP code.
|
||||
*/
|
||||
export const NoOtpCredentials: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
otpLogin: {
|
||||
userOtpCredentials: []
|
||||
},
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: () => false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorAndMultipleOtpCredentials:
|
||||
* - Purpose: Tests behavior when there is both an error in the OTP field and multiple OTP credentials.
|
||||
* - Scenario: Simulates the case where the user has multiple OTP credentials and encounters an error with the OTP input.
|
||||
* - Key Aspect: Ensures that the component can handle both multiple OTP credentials and display an error message simultaneously.
|
||||
*/
|
||||
export const WithErrorAndMultipleOtpCredentials: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
otpLogin: {
|
||||
userOtpCredentials: [
|
||||
{ id: "credential1", userLabel: "Device 1" },
|
||||
{ id: "credential2", userLabel: "Device 2" }
|
||||
],
|
||||
selectedCredentialId: "credential1"
|
||||
},
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (field: string) => field === "totp",
|
||||
get: () => "Invalid OTP code"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
40
src/login/pages/LoginPageExpired.stories.tsx
Normal file
40
src/login/pages/LoginPageExpired.stories.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-page-expired.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-page-expired.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorMessage:
|
||||
* - Purpose: Tests behavior when an error message is displayed along with the page expiration message.
|
||||
* - Scenario: Simulates a case where the session expired due to an error, and an error message is displayed alongside the expiration message.
|
||||
* - Key Aspect: Ensures that error messages are displayed correctly in addition to the page expiration notice.
|
||||
*/
|
||||
export const WithErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginRestartFlowUrl: "/mock-restart-flow",
|
||||
loginAction: "/mock-continue-login"
|
||||
},
|
||||
message: {
|
||||
type: "error",
|
||||
summary: "An error occurred while processing your session."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
68
src/login/pages/LoginPassword.stories.tsx
Normal file
68
src/login/pages/LoginPassword.stories.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-password.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-password.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithPasswordError:
|
||||
* - Purpose: Tests the behavior when an error occurs in the password field (e.g., incorrect password).
|
||||
* - Scenario: Simulates a scenario where an invalid password is entered, and an error message is displayed.
|
||||
* - Key Aspect: Ensures that the password input field displays error messages correctly.
|
||||
*/
|
||||
export const WithPasswordError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
resetPasswordAllowed: true
|
||||
},
|
||||
url: {
|
||||
loginAction: "/mock-login",
|
||||
loginResetCredentialsUrl: "/mock-reset-password"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (field: string) => field === "password",
|
||||
get: () => "Invalid password"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithoutResetPasswordOption:
|
||||
* - Purpose: Tests the behavior when the reset password option is disabled.
|
||||
* - Scenario: Simulates a scenario where the `resetPasswordAllowed` is set to `false`, and the "Forgot Password" link is not rendered.
|
||||
* - Key Aspect: Ensures that the component handles cases where resetting the password is not allowed.
|
||||
*/
|
||||
export const WithoutResetPasswordOption: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
resetPasswordAllowed: false
|
||||
},
|
||||
url: {
|
||||
loginAction: "/mock-login",
|
||||
loginResetCredentialsUrl: "/mock-reset-password"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: () => false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
96
src/login/pages/LoginResetOtp.stories.tsx
Normal file
96
src/login/pages/LoginResetOtp.stories.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-reset-otp.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-reset-otp.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithoutOtpCredentials:
|
||||
* - Purpose: Tests the behavior when no OTP credentials are available.
|
||||
* - Scenario: The component renders without any OTP credentials, showing only the submit button.
|
||||
* - Key Aspect: Ensures that the component handles the absence of OTP credentials correctly.
|
||||
*/
|
||||
export const WithoutOtpCredentials: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login"
|
||||
},
|
||||
configuredOtpCredentials: {
|
||||
userOtpCredentials: [],
|
||||
selectedCredentialId: undefined
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: () => false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithOtpError:
|
||||
* - Purpose: Tests the behavior when an error occurs with the OTP selection.
|
||||
* - Scenario: Simulates a scenario where an error occurs (e.g., no OTP selected), and an error message is displayed.
|
||||
* - Key Aspect: Ensures that error messages are displayed correctly for OTP-related errors.
|
||||
*/
|
||||
export const WithOtpError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login"
|
||||
},
|
||||
configuredOtpCredentials: {
|
||||
userOtpCredentials: [
|
||||
{ id: "otp1", userLabel: "Device 1" },
|
||||
{ id: "otp2", userLabel: "Device 2" }
|
||||
],
|
||||
selectedCredentialId: "otp1"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (field: string) => field === "totp",
|
||||
get: () => "Invalid OTP selection"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithOnlyOneOtpCredential:
|
||||
* - Purpose: Tests the behavior when there is only one OTP credential available.
|
||||
* - Scenario: Simulates the case where the user has only one OTP credential, and it is pre-selected by default.
|
||||
* - Key Aspect: Ensures that the component renders correctly with only one OTP credential pre-selected.
|
||||
*/
|
||||
export const WithOnlyOneOtpCredential: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login"
|
||||
},
|
||||
configuredOtpCredentials: {
|
||||
userOtpCredentials: [{ id: "otp1", userLabel: "Device 1" }],
|
||||
selectedCredentialId: "otp1"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: () => false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
60
src/login/pages/LoginResetPassword.stories.tsx
Normal file
60
src/login/pages/LoginResetPassword.stories.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-reset-password.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-reset-password.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
loginWithEmailAllowed: true,
|
||||
registrationEmailAsUsername: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
/**
|
||||
* WithUsernameError:
|
||||
* - Purpose: Tests behavior when an error occurs with the username input (e.g., invalid username).
|
||||
* - Scenario: The component displays an error message next to the username input field.
|
||||
* - Key Aspect: Ensures the username input shows error messages when validation fails.
|
||||
*/
|
||||
export const WithUsernameError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
loginWithEmailAllowed: false,
|
||||
registrationEmailAsUsername: false,
|
||||
duplicateEmailsAllowed: false
|
||||
},
|
||||
url: {
|
||||
loginAction: "/mock-login-action",
|
||||
loginUrl: "/mock-login-url"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (field: string) => field === "username",
|
||||
get: () => "Invalid username"
|
||||
},
|
||||
auth: {
|
||||
attemptedUsername: "invalid_user"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
30
src/login/pages/LoginUsername.stories.tsx
Normal file
30
src/login/pages/LoginUsername.stories.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-username.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-username.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
loginWithEmailAllowed: true,
|
||||
registrationEmailAsUsername: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
104
src/login/pages/LoginVerifyEmail.stories.tsx
Normal file
104
src/login/pages/LoginVerifyEmail.stories.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-verify-email.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-verify-email.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "You need to verify your email to activate your account.",
|
||||
type: "warning"
|
||||
},
|
||||
user: {
|
||||
email: "john.doe@gmail.com"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithSuccessMessage:
|
||||
* - Purpose: Tests when the email verification is successful, and the user receives a confirmation message.
|
||||
* - Scenario: The component renders a success message instead of a warning or error.
|
||||
* - Key Aspect: Ensures the success message is displayed correctly when the email is successfully verified.
|
||||
*/
|
||||
export const WithSuccessMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "Your email has been successfully verified.",
|
||||
type: "success"
|
||||
},
|
||||
user: {
|
||||
email: "john.doe@gmail.com"
|
||||
},
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorMessage:
|
||||
* - Purpose: Tests when there is an error during the email verification process.
|
||||
* - Scenario: The component renders an error message indicating the email verification failed.
|
||||
* - Key Aspect: Ensures the error message is shown correctly when the verification process encounters an issue.
|
||||
*/
|
||||
export const WithErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "There was an error verifying your email. Please try again.",
|
||||
type: "error"
|
||||
},
|
||||
user: {
|
||||
email: "john.doe@gmail.com"
|
||||
},
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithInfoMessage:
|
||||
* - Purpose: Tests when the user is prompted to verify their email without any urgency.
|
||||
* - Scenario: The component renders with an informational message for email verification.
|
||||
* - Key Aspect: Ensures the informational message is displayed properly.
|
||||
*/
|
||||
export const WithInfoMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "Please verify your email to continue using our services.",
|
||||
type: "info"
|
||||
},
|
||||
user: {
|
||||
email: "john.doe@gmail.com"
|
||||
},
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
278
src/login/pages/Register.stories.tsx
Normal file
278
src/login/pages/Register.stories.tsx
Normal file
|
@ -0,0 +1,278 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
import type { Attribute } from "keycloakify/login";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "register.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/register.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithEmailAlreadyExists: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
username: {
|
||||
value: "johndoe"
|
||||
},
|
||||
email: {
|
||||
value: "jhon.doe@gmail.com"
|
||||
},
|
||||
firstName: {
|
||||
value: "John"
|
||||
},
|
||||
lastName: {
|
||||
value: "Doe"
|
||||
}
|
||||
}
|
||||
},
|
||||
messagesPerField: {
|
||||
// NOTE: The other functions of messagesPerField are derived from get() and
|
||||
// existsError() so they are the only ones that need to mock.
|
||||
existsError: (fieldName: string, ...otherFieldNames: string[]) => [fieldName, ...otherFieldNames].includes("email"),
|
||||
get: (fieldName: string) => (fieldName === "email" ? "Email already exists." : undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithRestrictedToMITStudents: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
email: {
|
||||
validators: {
|
||||
pattern: {
|
||||
pattern: "^[^@]+@([^.]+\\.)*((mit\\.edu)|(berkeley\\.edu))$",
|
||||
"error-message": "${profile.attributes.email.pattern.error}"
|
||||
}
|
||||
},
|
||||
annotations: {
|
||||
inputHelperTextBefore: "${profile.attributes.email.inputHelperTextBefore}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-keycloakify": {
|
||||
messages: {
|
||||
"profile.attributes.email.inputHelperTextBefore": "Please use your MIT or Berkeley email.",
|
||||
"profile.attributes.email.pattern.error":
|
||||
"This is not an MIT (<strong>@mit.edu</strong>) nor a Berkeley (<strong>@berkeley.edu</strong>) email."
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithFavoritePet: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
favoritePet: {
|
||||
name: "favorite-pet",
|
||||
displayName: "${profile.attributes.favoritePet}",
|
||||
validators: {
|
||||
options: {
|
||||
options: ["cat", "dog", "fish"]
|
||||
}
|
||||
},
|
||||
annotations: {
|
||||
inputOptionLabelsI18nPrefix: "profile.attributes.favoritePet.options"
|
||||
},
|
||||
required: false,
|
||||
readOnly: false
|
||||
} satisfies Attribute
|
||||
}
|
||||
},
|
||||
"x-keycloakify": {
|
||||
messages: {
|
||||
"profile.attributes.favoritePet": "Favorite Pet",
|
||||
"profile.attributes.favoritePet.options.cat": "Fluffy Cat",
|
||||
"profile.attributes.favoritePet.options.dog": "Loyal Dog",
|
||||
"profile.attributes.favoritePet.options.fish": "Peaceful Fish"
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithNewsletter: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
newsletter: {
|
||||
name: "newsletter",
|
||||
displayName: "Sign up to the newsletter",
|
||||
validators: {
|
||||
options: {
|
||||
options: ["yes"]
|
||||
}
|
||||
},
|
||||
annotations: {
|
||||
inputOptionLabels: {
|
||||
yes: "I want my email inbox filled with spam"
|
||||
},
|
||||
inputType: "multiselect-checkboxes"
|
||||
},
|
||||
required: false,
|
||||
readOnly: false
|
||||
} satisfies Attribute
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
registrationEmailAsUsername: true
|
||||
},
|
||||
profile: {
|
||||
attributesByName: {
|
||||
username: undefined
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithRecaptcha: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
scripts: ["https://www.google.com/recaptcha/api.js?hl=en"],
|
||||
recaptchaRequired: true,
|
||||
recaptchaSiteKey: "6LfQHvApAAAAAE73SYTd5vS0lB1Xr7zdiQ-6iBVa"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithRecaptchaFrench: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
locale: {
|
||||
currentLanguageTag: "fr"
|
||||
},
|
||||
scripts: ["https://www.google.com/recaptcha/api.js?hl=fr"],
|
||||
recaptchaRequired: true,
|
||||
recaptchaSiteKey: "6LfQHvApAAAAAE73SYTd5vS0lB1Xr7zdiQ-6iBVa"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithPasswordMinLength8: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
passwordPolicies: {
|
||||
length: 8
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithTermsAcceptance: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
termsAcceptanceRequired: true,
|
||||
"x-keycloakify": {
|
||||
messages: {
|
||||
termsText: "<a href='https://example.com/terms'>Service Terms of Use</a>"
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithTermsNotAccepted: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
termsAcceptanceRequired: true,
|
||||
messagesPerField: {
|
||||
existsError: (fieldName: string) => fieldName === "termsAccepted",
|
||||
get: (fieldName: string) => (fieldName === "termsAccepted" ? "You must accept the terms." : undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithFieldErrors: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
username: { value: "" },
|
||||
email: { value: "invalid-email" }
|
||||
}
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (fieldName: string) => ["username", "email"].includes(fieldName),
|
||||
get: (fieldName: string) => {
|
||||
if (fieldName === "username") return "Username is required.";
|
||||
if (fieldName === "email") return "Invalid email format.";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithReadOnlyFields: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
username: { value: "johndoe", readOnly: true },
|
||||
email: { value: "jhon.doe@gmail.com", readOnly: false }
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithAutoGeneratedUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
username: { value: "autogenerated_username" }
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
104
src/login/pages/SelectAuthenticator.stories.tsx
Normal file
104
src/login/pages/SelectAuthenticator.stories.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "select-authenticator.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/select-authenticator.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithDifferentAuthenticationMethods: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
auth: {
|
||||
authenticationSelections: [
|
||||
{
|
||||
authExecId: "25697c4e-0c80-4f2c-8eb7-2c16347e8e8d",
|
||||
displayName: "auth-username-password-form-display-name",
|
||||
helpText: "auth-username-password-form-help-text",
|
||||
iconCssClass: "kcAuthenticatorPasswordClass"
|
||||
},
|
||||
{
|
||||
authExecId: "4cb60872-ce0d-4c8f-a806-e651ed77994b",
|
||||
displayName: "webauthn-passwordless-display-name",
|
||||
helpText: "webauthn-passwordless-help-text",
|
||||
iconCssClass: "kcAuthenticatorWebAuthnPasswordlessClass"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithRealmTranslations: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
auth: {
|
||||
authenticationSelections: [
|
||||
{
|
||||
authExecId: "f0c22855-eda7-4092-8565-0c22f77d2ffb",
|
||||
displayName: "home-idp-discovery-display-name",
|
||||
helpText: "home-idp-discovery-help-text",
|
||||
iconCssClass: "kcAuthenticatorDefaultClass"
|
||||
},
|
||||
{
|
||||
authExecId: "20456f5a-8b2b-45f3-98e0-551dcb27e3e1",
|
||||
displayName: "identity-provider-redirctor-display-name",
|
||||
helpText: "identity-provider-redirctor-help-text",
|
||||
iconCssClass: "kcAuthenticatorDefaultClass"
|
||||
},
|
||||
{
|
||||
authExecId: "eb435db9-474e-473a-8da7-c184fa510b96",
|
||||
displayName: "auth-username-password-form-display-name",
|
||||
helpText: "auth-username-password-help-text",
|
||||
iconCssClass: "kcAuthenticatorDefaultClass"
|
||||
}
|
||||
]
|
||||
},
|
||||
"x-keycloakify": {
|
||||
messages: {
|
||||
"home-idp-discovery-display-name": "Home identity provider",
|
||||
"home-idp-discovery-help-text":
|
||||
"Sign in via your home identity provider which will be automatically determined based on your provided email address.",
|
||||
"identity-provider-redirctor-display-name": "Identity Provider Redirector",
|
||||
"identity-provider-redirctor-help-text": "Sign in via your identity provider.",
|
||||
"auth-username-password-help-text": "Sign in via your username and password."
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithoutAuthenticationSelections:
|
||||
* - Purpose: Tests when no authentication methods are available for selection.
|
||||
* - Scenario: The component renders without any authentication options, providing a default message or fallback.
|
||||
* - Key Aspect: Ensures that the component gracefully handles the absence of available authentication methods.
|
||||
*/
|
||||
export const WithoutAuthenticationSelections: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
},
|
||||
auth: {
|
||||
authenticationSelections: [] // No authentication methods available
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
158
src/login/pages/WebauthnAuthenticate.stories.tsx
Normal file
158
src/login/pages/WebauthnAuthenticate.stories.tsx
Normal file
|
@ -0,0 +1,158 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "webauthn-authenticate.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/webauthn-authenticate.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithMultipleAuthenticators:
|
||||
* - Purpose: Tests when multiple WebAuthn authenticators are available for selection.
|
||||
* - Scenario: The component renders multiple authenticators, allowing the user to choose between them.
|
||||
* - Key Aspect: Ensures that the available authenticators are displayed, and the user can select one.
|
||||
*/
|
||||
export const WithMultipleAuthenticators: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
},
|
||||
authenticators: {
|
||||
authenticators: [
|
||||
{
|
||||
credentialId: "authenticator-1",
|
||||
label: "Security Key 1",
|
||||
transports: {
|
||||
iconClass: "kcAuthenticatorUsbIcon",
|
||||
displayNameProperties: ["USB"]
|
||||
},
|
||||
createdAt: "2023-01-01"
|
||||
},
|
||||
{
|
||||
credentialId: "authenticator-2",
|
||||
label: "Security Key 2",
|
||||
transports: {
|
||||
iconClass: "kcAuthenticatorNfcIcon",
|
||||
displayNameProperties: ["NFC"]
|
||||
},
|
||||
createdAt: "2023-02-01"
|
||||
}
|
||||
]
|
||||
},
|
||||
shouldDisplayAuthenticators: true
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithSingleAuthenticator:
|
||||
* - Purpose: Tests when only one WebAuthn authenticator is available.
|
||||
* - Scenario: The component renders the WebAuthn form with a single available authenticator.
|
||||
* - Key Aspect: Ensures the form renders correctly when there is only one authenticator available.
|
||||
*/
|
||||
export const WithSingleAuthenticator: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
},
|
||||
authenticators: {
|
||||
authenticators: [
|
||||
{
|
||||
credentialId: "authenticator-1",
|
||||
label: "My Security Key",
|
||||
transports: {
|
||||
iconClass: "kcAuthenticatorUsbIcon",
|
||||
displayNameProperties: ["USB"]
|
||||
},
|
||||
createdAt: "2023-01-01"
|
||||
}
|
||||
]
|
||||
},
|
||||
shouldDisplayAuthenticators: true
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorDuringAuthentication:
|
||||
* - Purpose: Tests the behavior when an error occurs during WebAuthn authentication.
|
||||
* - Scenario: The component renders with an error message displayed to the user.
|
||||
* - Key Aspect: Ensures the form handles authentication errors and displays a relevant message.
|
||||
*/
|
||||
export const WithErrorDuringAuthentication: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
},
|
||||
authenticators: {
|
||||
authenticators: [
|
||||
{
|
||||
credentialId: "authenticator-1",
|
||||
label: "My Security Key",
|
||||
transports: {
|
||||
iconClass: "kcAuthenticatorUsbIcon",
|
||||
displayNameProperties: ["USB"]
|
||||
},
|
||||
createdAt: "2023-01-01"
|
||||
}
|
||||
]
|
||||
},
|
||||
shouldDisplayAuthenticators: true,
|
||||
message: {
|
||||
summary: "An error occurred during WebAuthn authentication.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithJavaScriptDisabled:
|
||||
* - Purpose: Tests the behavior when JavaScript is disabled or not functioning.
|
||||
* - Scenario: The component renders a fallback message prompting the user to enable JavaScript for WebAuthn authentication.
|
||||
* - Key Aspect: Ensures the form provides a clear message when JavaScript is required but unavailable.
|
||||
*/
|
||||
export const WithJavaScriptDisabled: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
},
|
||||
authenticators: {
|
||||
authenticators: [
|
||||
{
|
||||
credentialId: "authenticator-1",
|
||||
label: "My Security Key",
|
||||
transports: {
|
||||
iconClass: "kcAuthenticatorUsbIcon",
|
||||
displayNameProperties: ["USB"]
|
||||
},
|
||||
createdAt: "2023-01-01"
|
||||
}
|
||||
]
|
||||
},
|
||||
shouldDisplayAuthenticators: true
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
61
src/login/pages/WebauthnRegister.stories.tsx
Normal file
61
src/login/pages/WebauthnRegister.stories.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "webauthn-register.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/webauthn-register.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithRetryAvailable:
|
||||
* - Purpose: Tests when the user is allowed to retry WebAuthn registration after a failure.
|
||||
* - Scenario: The component renders the form with a retry option.
|
||||
* - Key Aspect: Ensures the retry functionality is available and the form allows the user to retry.
|
||||
*/
|
||||
export const WithRetryAvailable: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
},
|
||||
isSetRetry: true,
|
||||
isAppInitiatedAction: false
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorDuringRegistration:
|
||||
* - Purpose: Tests when an error occurs during WebAuthn registration.
|
||||
* - Scenario: The component displays an error message related to WebAuthn registration failure.
|
||||
* - Key Aspect: Ensures the error message is displayed correctly, informing the user of the registration failure.
|
||||
*/
|
||||
export const WithErrorDuringRegistration: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
},
|
||||
isSetRetry: false,
|
||||
isAppInitiatedAction: false,
|
||||
message: {
|
||||
summary: "An error occurred during WebAuthn registration. Please try again.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
27
src/scss/background.scss
Normal file
27
src/scss/background.scss
Normal file
|
@ -0,0 +1,27 @@
|
|||
.ps-background {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
speak: none;
|
||||
|
||||
&--logo {
|
||||
width: 100px;
|
||||
margin: 0.1rem;
|
||||
}
|
||||
|
||||
&--1312 {
|
||||
transform: rotate(34deg);
|
||||
transform-origin: center center;
|
||||
color: #eee;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: -50vh;
|
||||
left: -50vw;
|
||||
font-weight: 900;
|
||||
font-size: 7rem;
|
||||
line-height: 6rem;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
}
|
||||
}
|
32
src/scss/button.scss
Normal file
32
src/scss/button.scss
Normal file
|
@ -0,0 +1,32 @@
|
|||
.ps-button {
|
||||
font-size: 1rem;
|
||||
padding: 0.5em 1em;
|
||||
line-height: 1.2em;
|
||||
border: 0.125em solid var(--foreground);
|
||||
border-radius: 1.5em;
|
||||
background-color: var(--background-darker-2);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
&_primary {
|
||||
border: 0.25em solid var(--foreground);
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-weight: bold;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: var(--foreground);
|
||||
color: var(--background);
|
||||
}
|
||||
}
|
||||
|
||||
&_small {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25em 0.7em;
|
||||
}
|
||||
}
|
19
src/scss/card.scss
Normal file
19
src/scss/card.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
.ps-card {
|
||||
background-color: var(--background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1rem solid var(--foreground);
|
||||
|
||||
&--header {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
&--title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--body {
|
||||
padding: 0rem 2rem;
|
||||
}
|
||||
}
|
10
src/scss/container.scss
Normal file
10
src/scss/container.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
.ps-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
}
|
19
src/scss/footer.scss
Normal file
19
src/scss/footer.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
.ps-footer {
|
||||
display: flex;
|
||||
margin-top: auto;
|
||||
z-index: 1;
|
||||
|
||||
&--link {
|
||||
&:hover {
|
||||
text-shadow: 0.2vw 0px 0px var(--accent);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 700px) {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
21
src/scss/form-group.scss
Normal file
21
src/scss/form-group.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
.ps-form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--label {
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ps-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&--error {
|
||||
margin-top: 0.25rem;
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
// font-family: monospace;
|
||||
}
|
||||
}
|
55
src/scss/header.scss
Normal file
55
src/scss/header.scss
Normal file
|
@ -0,0 +1,55 @@
|
|||
.ps-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
z-index: 100;
|
||||
align-items: flex-start;
|
||||
|
||||
&--title {
|
||||
font-size: 1.5rem;
|
||||
padding: 0 1rem;
|
||||
margin: 0;
|
||||
border-bottom: 0.5rem solid var(--foreground);
|
||||
background-color: var(--background);
|
||||
border-right: 0.5rem solid var(--foreground);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&--i18n {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&--nav {
|
||||
display: flex;
|
||||
border-bottom: 0.5rem solid var(--foreground);
|
||||
border-left: 0.5rem solid var(--foreground);
|
||||
background-color: var(--background);
|
||||
padding-left: 1rem;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&--nav-list {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--nav-item {
|
||||
margin: 0;
|
||||
margin-right: 1rem;
|
||||
|
||||
&-action {
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
src/scss/homelink.scss
Normal file
25
src/scss/homelink.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
.ps-homelink {
|
||||
pointer-events: all;
|
||||
color: var(--foreground);
|
||||
background: white;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
font-weight: 900;
|
||||
font-size: 24px;
|
||||
padding: 8px;
|
||||
line-height: 1em;
|
||||
text-shadow: 0.15vw 0px 0px white;
|
||||
transition: text-shadow 0.1s ease;
|
||||
border: 12px solid black;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
|
||||
&:hover {
|
||||
text-shadow: 0.3vw 0px 0px var(--accent);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
font-size: 32px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
18
src/scss/i18n-links.scss
Normal file
18
src/scss/i18n-links.scss
Normal file
|
@ -0,0 +1,18 @@
|
|||
.ps-i18n-links {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
pointer-events: all;
|
||||
color: var(--foreground);
|
||||
background: white;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
font-weight: 900;
|
||||
font-size: 24px;
|
||||
padding: 8px;
|
||||
line-height: 1em;
|
||||
text-shadow: 0.15vw 0px 0px var(--background);
|
||||
transition: text-shadow 0.1s ease;
|
||||
border: 12px solid var(--foreground);
|
||||
border-top: 0;
|
||||
border-right: 0;
|
||||
}
|
55
src/scss/index.scss
Normal file
55
src/scss/index.scss
Normal file
|
@ -0,0 +1,55 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
--accent: #ed1c24;
|
||||
--foreground: #000;
|
||||
--foreground-lighter-1: rgba(0, 0, 0, 0.7);
|
||||
--foreground-lighter-2: rgba(0, 0, 0, 0.3);
|
||||
--background: #fff;
|
||||
--background-darker-1: #f5f5f5;
|
||||
--background-darker-2: #eeeeee;
|
||||
|
||||
font-family: "Open Sans", Arial, sans-serif;
|
||||
font-weight: 800;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
box-sizing: border-box;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 0.2rem solid var(--accent);
|
||||
}
|
||||
|
||||
@import "./container";
|
||||
@import "./link";
|
||||
@import "./locale";
|
||||
@import "./card";
|
||||
@import "./button";
|
||||
@import "./input";
|
||||
@import "./table";
|
||||
@import "./form-group";
|
||||
|
||||
@import "./homelink";
|
||||
@import "./i18n-links";
|
||||
@import "./main";
|
||||
@import "./header";
|
||||
@import "./page";
|
||||
@import "./login-flow-pre";
|
||||
@import "./section-nav";
|
||||
@import "./logo";
|
||||
@import "./background";
|
||||
@import "./footer";
|
23
src/scss/input.scss
Normal file
23
src/scss/input.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
.ps-input {
|
||||
padding: 0.5rem 0.5rem;
|
||||
border: 2px solid var(--foreground-lighter-1);
|
||||
font-size: 1.2rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: var(--background);
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--background-darker-2);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--foreground-lighter-1);
|
||||
}
|
||||
}
|
||||
}
|
11
src/scss/link.scss
Normal file
11
src/scss/link.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.ps-link {
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom 0.1s ease;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 4px solid var(--accent);
|
||||
}
|
||||
}
|
62
src/scss/locale.scss
Normal file
62
src/scss/locale.scss
Normal file
|
@ -0,0 +1,62 @@
|
|||
.ps-locale {
|
||||
pointer-events: all;
|
||||
|
||||
&--wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
margin-left: auto;
|
||||
color: var(--foreground);
|
||||
background: white;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
font-weight: 900;
|
||||
font-size: 24px;
|
||||
padding: 8px;
|
||||
line-height: 1em;
|
||||
text-shadow: 0.15vw 0px 0px var(--background);
|
||||
transition: text-shadow 0.1s ease;
|
||||
border: 12px solid var(--foreground);
|
||||
border-top: 0;
|
||||
border-right: 0;
|
||||
|
||||
&:hover {
|
||||
text-shadow: 0.2vw 0px 0px var(--accent);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
font-size: 32px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--list {
|
||||
display: none;
|
||||
border: 8px solid var(--foreground);
|
||||
border-right: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: -8px;
|
||||
background: white;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&--item {
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
padding: 2px 8px;
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
text-shadow: 0.1vw 0px 0px var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
16
src/scss/login-flow-pre.scss
Normal file
16
src/scss/login-flow-pre.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
.ps-login-flow-pre {
|
||||
margin: 2rem;
|
||||
display: flex;
|
||||
|
||||
border-bottom: 0.25rem solid var(--foreground);
|
||||
padding-bottom: 0.25rem;
|
||||
|
||||
&--selected {
|
||||
font-weight: bold;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
&--cancel {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
57
src/scss/logo.scss
Normal file
57
src/scss/logo.scss
Normal file
|
@ -0,0 +1,57 @@
|
|||
.ps-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100px;
|
||||
|
||||
&--base {
|
||||
animation-name: rotate;
|
||||
animation-duration: 0.3s;
|
||||
animation-timing-function: linear;
|
||||
animation-play-state: paused;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
&:hover {
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2n) &--base {
|
||||
animation-delay: -0.1s;
|
||||
}
|
||||
&:nth-child(3n) &--base {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
&:nth-child(5n) &--base {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
&:nth-child(7n) &--base {
|
||||
animation-delay: -7s;
|
||||
}
|
||||
&:nth-child(11n) &--base {
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
&:nth-child(13n) &--base {
|
||||
animation-delay: -1s;
|
||||
}
|
||||
|
||||
&--base::before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
38
src/scss/main.scss
Normal file
38
src/scss/main.scss
Normal file
|
@ -0,0 +1,38 @@
|
|||
.ps-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding-top: 10vw;
|
||||
padding-bottom: 2vw;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
&--background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&--page {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&_full {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&_full &--page {
|
||||
@media screen and (min-width: 1200px) {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
3
src/scss/nav.scss
Normal file
3
src/scss/nav.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.ps-nav {
|
||||
display: flex;
|
||||
}
|
152
src/scss/page.scss
Normal file
152
src/scss/page.scss
Normal file
|
@ -0,0 +1,152 @@
|
|||
.ps-page {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
padding-top: 4rem;
|
||||
position: relative;
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
padding: 1vw;
|
||||
padding-top: 4rem;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&_home {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&--header {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: auto;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
&--title {
|
||||
font-size: 2rem;
|
||||
border-bottom: 0.5rem solid var(--foreground);
|
||||
padding-bottom: 0.5rem;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
&--section {
|
||||
border: 12px solid black;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
max-width: 700px;
|
||||
flex-basis: 100%;
|
||||
flex-shrink: 1;
|
||||
pointer-events: all;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
pointer-events: all;
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
margin: 1vw;
|
||||
}
|
||||
|
||||
&_home {
|
||||
padding: 5vw;
|
||||
}
|
||||
|
||||
&_full {
|
||||
max-width: unset;
|
||||
width: calc(100vw - 1.6rem);
|
||||
margin: 0.8rem;
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
width: 96vw;
|
||||
margin: 1vw;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom 0.1s ease;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 4px solid var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 230px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--section-link {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--background);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
padding-left: 132px;
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
display: none;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&--section-contents {
|
||||
margin: 2rem;
|
||||
margin-bottom: 0;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 5vw;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
background-color: var(--background-darker-2);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
pre {
|
||||
border: 1px solid var(--foreground-lighter-2);
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
> .ps-table {
|
||||
margin-top: 1rem;
|
||||
|
||||
+ * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
> h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin-top: 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
38
src/scss/section-nav.scss
Normal file
38
src/scss/section-nav.scss
Normal file
|
@ -0,0 +1,38 @@
|
|||
.ps-section-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: var(--background);
|
||||
border-bottom: 2px solid var(--foreground);
|
||||
overflow-x: auto;
|
||||
|
||||
&--list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
margin: 0 -0.5rem;
|
||||
}
|
||||
|
||||
&--link {
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
&_active {
|
||||
a {
|
||||
color: var(--foreground);
|
||||
border-bottom: 4px solid var(--foreground);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
37
src/scss/table.scss
Normal file
37
src/scss/table.scss
Normal file
|
@ -0,0 +1,37 @@
|
|||
.ps-table {
|
||||
overflow: auto;
|
||||
border-collapse: collapse;
|
||||
margin-left: -0.25rem;
|
||||
width: calc(100% + 0.25rem);
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0.1rem 0.25rem;
|
||||
}
|
||||
|
||||
tr {
|
||||
border: 0;
|
||||
border-left: 0.25rem solid transparent;
|
||||
|
||||
&:nth-child(2n) {
|
||||
background-color: var(--background-darker-1);
|
||||
border-left: 0.25rem solid var(--background-darker-1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-darker-2);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
thead tr {
|
||||
border-bottom: 4px solid var(--foreground-lighter-1);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
thead td,
|
||||
th {
|
||||
padding: 0.1rem 0.5rem;
|
||||
}
|
||||
}
|
322
src/scss/typography.scss
Normal file
322
src/scss/typography.scss
Normal file
|
@ -0,0 +1,322 @@
|
|||
/* To regenerate this file, run $ google-font-downloader 'https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,800;1,400;1,800&display=swap' */
|
||||
|
||||
/* cyrillic-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
// }
|
||||
// /* cyrillic */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
// }
|
||||
// /* greek-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+1F00-1FFF;
|
||||
// }
|
||||
// /* greek */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0370-03FF;
|
||||
// }
|
||||
// /* hebrew */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
|
||||
// }
|
||||
// /* vietnamese */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
// }
|
||||
// /* latin-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
// }
|
||||
// /* latin */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6FxZCJgg.woff2) format('woff2');
|
||||
// unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
// }
|
||||
// /* cyrillic-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
// }
|
||||
// /* cyrillic */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
// }
|
||||
// /* greek-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+1F00-1FFF;
|
||||
// }
|
||||
// /* greek */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0370-03FF;
|
||||
// }
|
||||
// /* hebrew */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
|
||||
// }
|
||||
// /* vietnamese */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
// }
|
||||
// /* latin-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06FxZCJgvAQ.woff2) format('woff2');
|
||||
// unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
// }
|
||||
// /* latin */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: italic;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6FxZCJgg.woff2) format('woff2');
|
||||
// unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
// }
|
||||
// /* cyrillic-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
// }
|
||||
// /* cyrillic */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
// }
|
||||
// /* greek-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+1F00-1FFF;
|
||||
// }
|
||||
// /* greek */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0370-03FF;
|
||||
// }
|
||||
// /* hebrew */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
|
||||
// }
|
||||
// /* vietnamese */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
// }
|
||||
// /* latin-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
// }
|
||||
// /* latin */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2) format('woff2');
|
||||
// unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
// }
|
||||
// /* cyrillic-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
// }
|
||||
// /* cyrillic */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
// }
|
||||
// /* greek-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+1F00-1FFF;
|
||||
// }
|
||||
// /* greek */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0370-03FF;
|
||||
// }
|
||||
// /* hebrew */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
|
||||
// }
|
||||
// /* vietnamese */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
// }
|
||||
// /* latin-ext */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu0SC55K5gw.woff2) format('woff2');
|
||||
// unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
// }
|
||||
// /* latin */
|
||||
// @font-face {
|
||||
// font-family: 'Open Sans';
|
||||
// font-style: normal;
|
||||
// font-weight: 800;
|
||||
// font-stretch: 100%;
|
||||
// font-display: swap;
|
||||
// src: url(fonts/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2) format('woff2');
|
||||
// unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
// }
|
Loading…
Reference in a new issue