diff --git a/.formatter.exs b/.formatter.exs
index d165150..ff8ae9c 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -1,6 +1,5 @@
[
- import_deps: [:phoenix, :surface, :ecto],
- inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
- surface_inputs: ["{lib,test}/**/*.{ex,exs,sface}"],
- subdirectories: []
+ import_deps: [:phoenix, :surface],
+ plugins: [Phoenix.LiveView.HTMLFormatter, Surface.Formatter.Plugin],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "{lib,test}/**/*.sface"]
]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ac8221a..822e2c9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,31 +14,32 @@ jobs:
fail-fast: false
matrix:
include:
- - elixir: 1.12.0
- otp: 24.0
- - elixir: 1.13.4
- otp: 24.0
- - elixir: 1.13.4
- otp: 25.0
- check_formatted: true
- warnings_as_errors: true
- - elixir: 1.14.0
- otp: 25.0
+ - elixir: '1.14.5'
+ otp: '25.3.2.8'
env:
MIX_ENV: test
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
+ - name: Deps and _build cache
+ uses: actions/cache@v4
+ id: deps-cache
+ with:
+ path: |
+ deps
+ _build
+ key: ${{ runner.os }}-${{ runner.arch }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
- name: Install Dependencies
+ if: steps.deps-cache.outputs.cache-hit != 'true'
run: |
mix local.hex --force
mix local.rebar --force
mix deps.get --only test
+ - run: mix deps.compile
+ if: steps.deps-cache.outputs.cache-hit != 'true'
- run: mix compile --warnings-as-errors
- if: matrix.warnings_as_errors
- run: mix format --check-formatted
- if: matrix.check_formatted
- run: mix test
diff --git a/.gitignore b/.gitignore
index 5745e81..751c063 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,31 +19,24 @@ erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
+# Temporary files, for example, from tests.
+/tmp/
+
# Ignore package tarball (built via "mix hex.build").
surface_site-*.tar
-# If NPM crashes, it generates a log, let's ignore it too.
-npm-debug.log
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
-# The directory NPM downloads your dependencies sources to.
-/assets/node_modules/
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
-# Since we are building assets from assets/,
-# we ignore priv/static. You may want to comment
-# this depending on your deployment strategy.
-# /priv/static/
+# In case you use Node.js/npm, you want to ignore these.
+npm-debug.log
+/assets/node_modules/
-# Ignore assets that are produced by build tools.
-/priv/static/assets/
+# Ignore generated js hook files for components
+assets/js/_hooks/
-# Files matching config/*.secret.exs pattern contain sensitive
-# data and you should not commit them into version control.
-#
-# Alternatively, you may comment the line below and commit the
-# secrets files as long as you replace their contents by environment
-# variables.
-/config/*.secret.exs
-
-# Ignore auto-generated hook files
-/assets/js/_hooks/
-/assets/css/_components.css
+# Ignore generated CSS file for components
+assets/css/_components.css
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 0000000..bde48fc
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1,2 @@
+erlang 25.3.2.8
+elixir 1.14.5-otp-25
diff --git a/README.md b/README.md
index ae94252..e0b9952 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
# Surface Site
+
Documentation website for the [Surface](https://surface-ui.org/) project.
## Development
To start your Phoenix server:
- * Run `mix setup`
- * Start Phoenix endpoint with `mix phx.server`
+* Run `mix setup`
+* Start Phoenix endpoint with `mix phx.server`
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
diff --git a/assets/css/app.css b/assets/css/app.css
index 23b4421..cf52003 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -1,3 +1,13 @@
+/* Import scoped CSS rules for components
+@import "./_components.css";
+
+TODO: Tailwind
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
+
+This file is for your main application CSS */
+
body.mobile-sidebar-open {
overflow: hidden;
position: fixed;
@@ -30,6 +40,7 @@ code {
margin-top: .2em;
margin-bottom: .3em;
}
+
.menu-list li a {
padding: .4em .75em;
}
@@ -40,7 +51,7 @@ code {
position: absolute;
right: 0;
top: 0;
- background: hsla(0,0%,4%,.86);
+ background: hsla(0, 0%, 4%, .86);
position: fixed;
z-index: 40;
}
@@ -64,7 +75,7 @@ code {
.mobile-sidebar .sidebar-content {
background-color: #f5f5f5;
- box-shadow: 5px 0 13px 3px hsla(0,0%,4%,.1);
+ box-shadow: 5px 0 13px 3px hsla(0, 0%, 4%, .1);
width: 260px;
z-index: 41;
}
@@ -121,7 +132,7 @@ code {
.dark.card {
border-radius: 10px;
- box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1);
+ box-shadow: 0 2px 3px rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .1);
}
.dark.card > .card-content {
@@ -203,7 +214,7 @@ code {
color: #7a7a7a;
}
-#ComponentInfo_Markdown div.Example pre > code{
+#ComponentInfo_Markdown div.Example pre > code {
line-height: 1.6;
}
@@ -239,11 +250,12 @@ code {
/* SurfaceSiteWeb.Components.ComponentAPI */
-.ComponentAPI table{
+.ComponentAPI table {
font-size: .9rem;
}
-.ComponentAPI .table td, .table th {
+.ComponentAPI .table td,
+.table th {
line-height: 1.5;
}
@@ -256,7 +268,7 @@ code {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
- color: rgba(0,0,0,.7);
+ color: rgba(0, 0, 0, .7);
margin: 2.25rem 0;
}
@@ -416,31 +428,29 @@ pre > code[class*="language-"] .token.attr-value .token.punctuation.string {
.markdown pre.line-numbers,
pre[class*="language-"].line-numbers {
- position: relative;
- padding-left: 3.8em;
- counter-reset: linenumber;
+ position: relative;
+ padding-left: 3.8em;
+ counter-reset: linenumber;
}
pre[class*="language-"].line-numbers > code {
- position: relative;
- white-space: inherit;
+ position: relative;
+ white-space: inherit;
}
.line-numbers .line-numbers-rows {
- position: absolute;
- pointer-events: none;
- top: 0;
- font-size: 100%;
- left: -3.8em;
- width: 3em; /* works for line-numbers below 1000 lines */
- letter-spacing: -1px;
- border-right: 1px solid #999;
-
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-
+ position: absolute;
+ pointer-events: none;
+ top: 0;
+ font-size: 100%;
+ left: -3.8em;
+ width: 3em; /* works for line-numbers below 1000 lines */
+ letter-spacing: -1px;
+ border-right: 1px solid #999;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
}
.line-numbers-rows > span {
@@ -459,76 +469,75 @@ pre[class*="language-"].line-numbers > code {
/* Prism's line-highlight plugin */
pre[data-line] {
- position: relative;
- padding: 1em 0 1em 3em;
+ position: relative;
+ padding: 1em 0 1em 3em;
}
.line-highlight {
- position: absolute;
- left: 0;
- right: 0;
- padding: inherit 0;
+ position: absolute;
+ left: 0;
+ right: 0;
+ padding: inherit 0;
/* Same as .prism’s padding-top */
- /* margin-top: 1em; */
- margin-top: 0.85em;
-
- background: hsla(24, 20%, 50%,.08);
- background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
-
- pointer-events: none;
-
- line-height: inherit;
- white-space: pre;
+ /* margin-top: 1em; */
+ margin-top: 0.85em;
+ background: hsla(24, 20%, 50%, .08);
+ background: linear-gradient(to right, hsla(24, 20%, 50%, .1) 70%, hsla(24, 20%, 50%, 0));
+ pointer-events: none;
+ line-height: inherit;
+ white-space: pre;
}
@media print {
- .line-highlight {
- /*
+ .line-highlight {
+ /*
* This will prevent browsers from replacing the background color with white.
* It's necessary because the element is layered on top of the displayed code.
*/
- -webkit-print-color-adjust: exact;
- color-adjust: exact;
- }
-}
-
- .line-highlight:before,
- .line-highlight[data-end]:after {
- content: attr(data-start);
- position: absolute;
- top: .4em;
- left: .6em;
- min-width: 1em;
- padding: 0 .5em;
- background-color: hsla(24, 20%, 50%,.4);
- color: hsl(24, 20%, 95%);
- font: bold 65%/1.5 sans-serif;
- text-align: center;
- vertical-align: .3em;
- border-radius: 999px;
- text-shadow: none;
- box-shadow: 0 1px white;
- }
-
- .line-highlight[data-end]:after {
- content: attr(data-end);
- top: auto;
- bottom: .4em;
- }
+ -webkit-print-color-adjust: exact;
+ color-adjust: exact;
+ }
+}
+
+.line-highlight:before,
+.line-highlight[data-end]:after {
+ content: attr(data-start);
+ position: absolute;
+ top: .4em;
+ left: .6em;
+ min-width: 1em;
+ padding: 0 .5em;
+ background-color: hsla(24, 20%, 50%, .4);
+ color: hsl(24, 20%, 95%);
+ font: bold 65%/1.5 sans-serif;
+ text-align: center;
+ vertical-align: .3em;
+ border-radius: 999px;
+ text-shadow: none;
+ box-shadow: 0 1px white;
+}
+
+.line-highlight[data-end]:after {
+ content: attr(data-end);
+ top: auto;
+ bottom: .4em;
+}
.line-numbers .line-highlight:before,
.line-numbers .line-highlight:after {
- content: none;
+ content: none;
}
pre[id].linkable-line-numbers span.line-numbers-rows {
- pointer-events: all;
+ pointer-events: all;
}
+
pre[id].linkable-line-numbers span.line-numbers-rows > span:before {
- cursor: pointer;
+ cursor: pointer;
}
+
pre[id].linkable-line-numbers span.line-numbers-rows > span:hover:before {
- background-color: rgba(128, 128, 128, .2);
+ background-color: rgba(128, 128, 128, .2);
}
pre.language-mermaid {
@@ -568,7 +577,7 @@ div#nprogress .bar .peg {
/* Customize vsc-dark-plus theme */
-.token.slotable-name.class-name{
+.token.slotable-name.class-name {
color: #9cdcfe;
}
diff --git a/assets/js/app.js b/assets/js/app.js
index 98b1a5f..04ba908 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -6,22 +6,45 @@ import "../css/app.css";
window.Prism = window.Prism || {};
window.Prism.manual = true;
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
+
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import Prism from "../vendor/prism.js";
import "../vendor/font-awesome.js";
import Hooks from "./_hooks"
+
+let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
+let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks})
+
// Uncomment when we start using mermaid
// import mermaid from "../vendor/mermaid"
// mermaid.initialize({startOnLoad: false});
-// Don't load the topbar for catalogue examples/praygrounds
+// Don't load the topbar for catalogue examples/playgrounds
if (!window.frameElement) {
topbar.config({barColors: {0: "hsl(0, 0%, 86%)"}})
- window.addEventListener("phx:page-loading-start", info => topbar.show())
- window.addEventListener("phx:page-loading-stop", info => topbar.hide())
+ window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
+ window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
}
Hooks["Highlight"] = {
@@ -52,7 +75,11 @@ Hooks["SectionHeading"] = {
}
}
-let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
-let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})
+// connect if there are any LiveViews on the page
liveSocket.connect()
-// window.liveSocket = liveSocket
+
+// expose liveSocket on window for web console debug logs and latency simulation:
+// >> liveSocket.enableDebug()
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
+window.liveSocket = liveSocket
diff --git a/assets/package.json b/assets/package.json
deleted file mode 100644
index 79c82d4..0000000
--- a/assets/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "scripts": {
- "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild"
- }
-}
diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js
new file mode 100644
index 0000000..b779942
--- /dev/null
+++ b/assets/tailwind.config.js
@@ -0,0 +1,71 @@
+// TODO: Tailwind
+// // See the Tailwind configuration guide for advanced usage
+// // https://tailwindcss.com/docs/configuration
+
+// const plugin = require("tailwindcss/plugin")
+// const fs = require("fs")
+// const path = require("path")
+
+// module.exports = {
+// content: [
+// "./js/**/*.js",
+// "../lib/*_web.ex",
+// "../lib/*_web/**/*.*ex",
+// "../lib/*_web/**/*.sface",
+// "../priv/catalogue/**/*.{ex,sface}"
+// ],
+// theme: {
+// extend: {
+// colors: {
+// brand: "#FD4F00",
+// }
+// },
+// },
+// plugins: [
+// require("@tailwindcss/forms"),
+// // Allows prefixing tailwind classes with LiveView classes to add rules
+// // only when LiveView classes are applied, for example:
+// //
+// //
+// //
+// plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
+// plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
+// plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
+// plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
+
+// // Embeds Heroicons (https://heroicons.com) into your app.css bundle
+// // See your `CoreComponents.icon/1` for more information.
+// //
+// plugin(function({matchComponents, theme}) {
+// let iconsDir = path.join(__dirname, "./vendor/heroicons/optimized")
+// let values = {}
+// let icons = [
+// ["", "/24/outline"],
+// ["-solid", "/24/solid"],
+// ["-mini", "/20/solid"]
+// ]
+// icons.forEach(([suffix, dir]) => {
+// fs.readdirSync(path.join(iconsDir, dir)).map(file => {
+// let name = path.basename(file, ".svg") + suffix
+// values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
+// })
+// })
+// matchComponents({
+// "hero": ({name, fullPath}) => {
+// let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
+// return {
+// [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
+// "-webkit-mask": `var(--hero-${name})`,
+// "mask": `var(--hero-${name})`,
+// "mask-repeat": "no-repeat",
+// "background-color": "currentColor",
+// "vertical-align": "middle",
+// "display": "inline-block",
+// "width": theme("spacing.5"),
+// "height": theme("spacing.5")
+// }
+// }
+// }, {values})
+// })
+// ]
+// }
diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js
index ff7fbb6..4195727 100644
--- a/assets/vendor/topbar.js
+++ b/assets/vendor/topbar.js
@@ -1,7 +1,7 @@
/**
* @license MIT
- * topbar 1.0.0, 2021-01-06
- * http://buunguyen.github.io/topbar
+ * topbar 2.0.0, 2023-02-04
+ * https://buunguyen.github.io/topbar
* Copyright (c) 2021 Buu Nguyen
*/
(function (window, document) {
@@ -35,10 +35,11 @@
})();
var canvas,
- progressTimerId,
- fadeTimerId,
currentProgress,
showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
addEvent = function (elem, type, handler) {
if (elem.addEventListener) elem.addEventListener(type, handler, false);
else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
@@ -95,21 +96,26 @@
for (var key in opts)
if (options.hasOwnProperty(key)) options[key] = opts[key];
},
- show: function () {
+ show: function (delay) {
if (showing) return;
- showing = true;
- if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
- if (!canvas) createCanvas();
- canvas.style.opacity = 1;
- canvas.style.display = "block";
- topbar.progress(0);
- if (options.autoRun) {
- (function loop() {
- progressTimerId = window.requestAnimationFrame(loop);
- topbar.progress(
- "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
- );
- })();
+ if (delay) {
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), delay);
+ } else {
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
}
},
progress: function (to) {
@@ -125,6 +131,8 @@
return currentProgress;
},
hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
if (!showing) return;
showing = false;
if (progressTimerId != null) {
diff --git a/config/config.exs b/config/config.exs
index 45c34cf..49b7b0c 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,5 +1,5 @@
# This file is responsible for configuring your application
-# and its dependencies with the aid of the Mix.Config module.
+# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
@@ -7,25 +7,6 @@
# General application configuration
import Config
-# Configures the endpoint
-config :surface_site, SurfaceSiteWeb.Endpoint,
- url: [host: "localhost"],
- secret_key_base: "qdZaVqRjkIyGWZ50fXKWgziVNqwZTtBLxQiTxBHJpMGXJvDljm+oAEwq+4r+2R4y",
- render_errors: [view: SurfaceSiteWeb.ErrorView, accepts: ~w(json)],
- pubsub_server: SurfaceSite.PubSub,
- check_origin: true,
- live_view: [
- signing_salt: "eEXUz84u6QjVdwt4cgT1rDU12+C9NuDn"
- ]
-
-# Configures Elixir's Logger
-config :logger, :console,
- format: "$time $metadata[$level] $message\n",
- metadata: [:request_id]
-
-# Use Jason for JSON parsing in Phoenix
-config :phoenix, :json_library, Jason
-
# Configures Surface components
config :surface, :components, [
{Surface.Components.Markdown,
@@ -43,15 +24,50 @@ config :surface, :components, [
{SurfaceSiteWeb.Contexts.Example01.Field, propagate_context_to_slots: true}
]
+# Configures the endpoint
+config :surface_site, SurfaceSiteWeb.Endpoint,
+ url: [host: "localhost"],
+ render_errors: [
+ formats: [html: SurfaceSiteWeb.ErrorHTML, json: SurfaceSiteWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: SurfaceSite.PubSub,
+ check_origin: true,
+ live_view: [
+ signing_salt: "eEXUz84u6QjVdwt4cgT1rDU12+C9NuDn"
+ ]
+
# Configure esbuild (the version is required)
config :esbuild,
- version: "0.14.10",
+ version: "0.17.11",
default: [
- args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets),
+ args:
+ ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
+# TODO: Tailwind
+# Configure tailwind (the version is required)
+# config :tailwind,
+# version: "3.3.2",
+# default: [
+# args: ~w(
+# --config=tailwind.config.js
+# --input=css/app.css
+# --output=../priv/static/assets/app.css
+# ),
+# cd: Path.expand("../assets", __DIR__)
+# ]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
-import_config "#{Mix.env()}.exs"
+import_config "#{config_env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
index be3e290..3db7c95 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -4,22 +4,20 @@ import Config
# debugging and code reloading.
#
# The watchers configuration can be used to run external
-# watchers to your application. For example, we use it
-# with webpack to recompile .js and .css sources.
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
config :surface_site, SurfaceSiteWeb.Endpoint,
- http: [port: 4000],
- debug_errors: true,
- code_reloader: true,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: 4000],
check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "qdZaVqRjkIyGWZ50fXKWgziVNqwZTtBLxQiTxBHJpMGXJvDljm+oAEwq+4r+2R4y",
watchers: [
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
- ],
- reloadable_compilers: [:phoenix, :elixir, :surface],
- live_reload: [
- patterns: [
- ~r{lib/surface_site_web/(live|components)/.*(ex|js)$},
- ~r{priv/posts/*/.*(md)$}
- ]
+ # TODO: Tailwind
+ # tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
]
# ## SSL Support
@@ -30,7 +28,6 @@ config :surface_site, SurfaceSiteWeb.Endpoint,
#
# mix phx.gen.cert
#
-# Note that this task requires Erlang/OTP 20 or later.
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
@@ -46,6 +43,17 @@ config :surface_site, SurfaceSiteWeb.Endpoint,
# configured to run both http and https servers on
# different ports.
+# Watch static and templates for browser reloading.
+config :surface_site, SurfaceSiteWeb.Endpoint,
+ reloadable_compilers: [:elixir, :app, :surface],
+ live_reload: [
+ patterns: [
+ ~r{priv/posts/*/.*(md)$},
+ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"lib/surface_site_web/(controllers|live|components)/.*(ex|heex|sface|js)$"
+ ]
+ ]
+
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
diff --git a/config/prod.exs b/config/prod.exs
index da26abf..19ade0b 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -1,76 +1,16 @@
import Config
-# For production, don't forget to configure the url host
-# to something meaningful, Phoenix uses this information
-# when generating URLs.
-#
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
-# manifest is generated by the `mix phx.digest` task,
+# manifest is generated by the `mix assets.deploy` task,
# which you should run after static files are built and
# before starting your production server.
config :surface_site, SurfaceSiteWeb.Endpoint,
- http: [
- port: System.get_env("PORT") || 4000,
- transport_options: [socket_opts: [:inet6]]
- ],
- url: [scheme: "https", host: "surface-ui.org", port: 443],
force_ssl: [hsts: true, rewrite_on: [:x_forwarded_proto]],
- secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE"),
cache_static_manifest: "priv/static/cache_manifest.json"
# Do not print debug messages in production
config :logger, level: :info
-# ## SSL Support
-#
-# To get SSL working, you will need to add the `https` key
-# to the previous section and set your `:url` port to 443:
-#
-# config :surface_site, SurfaceSiteWeb.Endpoint,
-# ...
-# url: [host: "example.com", port: 443],
-# https: [
-# :inet6,
-# port: 443,
-# cipher_suite: :strong,
-# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
-# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
-# ]
-#
-# The `cipher_suite` is set to `:strong` to support only the
-# latest and more secure SSL ciphers. This means old browsers
-# and clients may not be supported. You can set it to
-# `:compatible` for wider support.
-#
-# `:keyfile` and `:certfile` expect an absolute path to the key
-# and cert in disk or a relative path inside priv, for example
-# "priv/ssl/server.key". For all supported SSL configuration
-# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
-#
-# We also recommend setting `force_ssl` in your endpoint, ensuring
-# no data is ever sent via http, always redirecting to https:
-#
-# config :surface_site, SurfaceSiteWeb.Endpoint,
-# force_ssl: [hsts: true]
-#
-# Check `Plug.SSL` for all available options in `force_ssl`.
-
-# ## Using releases (distillery)
-#
-# If you are doing OTP releases, you need to instruct Phoenix
-# to start the server for all endpoints:
-#
-# config :phoenix, :serve_endpoints, true
-#
-# Alternatively, you can configure exactly which server to
-# start per endpoint:
-#
-# config :surface_site, SurfaceSiteWeb.Endpoint, server: true
-#
-# Note you can't rely on `System.get_env/1` when using releases.
-# See the releases documentation accordingly.
-
-# Finally import the config/prod.secret.exs which should be versioned
-# separately.
-# import_config "prod.secret.exs"
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/config/runtime.exs b/config/runtime.exs
new file mode 100644
index 0000000..2ea1020
--- /dev/null
+++ b/config/runtime.exs
@@ -0,0 +1,82 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/surface_site start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :surface_site, SurfaceSiteWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "surface-ui.org"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :surface_site, SurfaceSiteWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :surface_site, SurfaceSiteWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your endpoint, ensuring
+ # no data is ever sent via http, always redirecting to https:
+ #
+ # config :surface_site, SurfaceSiteWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+end
diff --git a/config/test.exs b/config/test.exs
index 04e2141..82cd81d 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -3,8 +3,15 @@ import Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :surface_site, SurfaceSiteWeb.Endpoint,
- http: [port: 4002],
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "qick86rrt0cZHQpbf7BLKywNWLsT9FUWpg/f//xOmOw6ZD/rVBUd+f0KMzEw0jI+",
server: false
+# In test we don't send emails.
+config :surface_site, SurfaceSite.Mailer, adapter: Swoosh.Adapters.Test
+
# Print only warnings and errors during test
-config :logger, level: :warn
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/lib/surface_site/application.ex b/lib/surface_site/application.ex
index 870fc96..c5505a0 100644
--- a/lib/surface_site/application.ex
+++ b/lib/surface_site/application.ex
@@ -5,10 +5,12 @@ defmodule SurfaceSite.Application do
use Application
+ @impl true
def start(_type, _args) do
- # List all child processes to be supervised
children = [
+ # Start the PubSub system
{Phoenix.PubSub, name: SurfaceSite.PubSub},
+ # Start the Endpoint (http/https)
SurfaceSiteWeb.Endpoint
]
@@ -20,6 +22,7 @@ defmodule SurfaceSite.Application do
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
+ @impl true
def config_change(changed, _new, removed) do
SurfaceSiteWeb.Endpoint.config_change(changed, removed)
:ok
diff --git a/lib/surface_site_web.ex b/lib/surface_site_web.ex
index 64de852..2a8500c 100644
--- a/lib/surface_site_web.ex
+++ b/lib/surface_site_web.ex
@@ -1,61 +1,105 @@
defmodule SurfaceSiteWeb do
@moduledoc """
The entrypoint for defining your web interface, such
- as controllers, views, channels and so on.
+ as controllers, components, channels, and so on.
This can be used in your application as:
use SurfaceSiteWeb, :controller
- use SurfaceSiteWeb, :view
+ use SurfaceSiteWeb, :html
- The definitions below will be executed for every view,
- controller, etc, so keep them short and clean, focused
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
- below. Instead, define any helper function in modules
- and import those modules here.
+ below. Instead, define additional modules and import
+ those modules here.
"""
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ # Import common connection and controller functions to use in pipelines
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
def controller do
quote do
- use Phoenix.Controller, namespace: SurfaceSiteWeb
+ use Phoenix.Controller,
+ formats: [:html, :json],
+ layouts: [html: SurfaceSiteWeb.Layouts]
import Plug.Conn
- import Phoenix.LiveView.Controller, only: [live_render: 3]
- alias SurfaceSiteWeb.Router.Helpers, as: Routes
+
+ unquote(verified_routes())
end
end
- def view do
+ def live_view do
quote do
- use Phoenix.View,
- root: "lib/surface_site_web/templates",
- namespace: SurfaceSiteWeb
+ use Phoenix.LiveView,
+ layout: {SurfaceSiteWeb.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
# Import convenience functions from controllers
- import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
- import Phoenix.LiveView.Helpers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
- alias SurfaceSiteWeb.Router.Helpers, as: Routes
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
import Surface
-
- use Phoenix.HTML
end
end
- def router do
+ defp html_helpers do
quote do
- use Phoenix.Router
- import Plug.Conn
- import Phoenix.Controller
- import Phoenix.LiveView.Router
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components and translation
+ import SurfaceSiteWeb.CoreComponents
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
end
end
- def channel do
+ def verified_routes do
quote do
- use Phoenix.Channel
+ use Phoenix.VerifiedRoutes,
+ endpoint: SurfaceSiteWeb.Endpoint,
+ router: SurfaceSiteWeb.Router,
+ statics: SurfaceSiteWeb.static_paths()
end
end
@@ -65,4 +109,13 @@ defmodule SurfaceSiteWeb do
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
+
+ def surface_live_view do
+ quote do
+ use Surface.LiveView,
+ layout: {SurfaceSiteWeb.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
end
diff --git a/lib/surface_site_web/components/core_components.ex b/lib/surface_site_web/components/core_components.ex
new file mode 100644
index 0000000..5220267
--- /dev/null
+++ b/lib/surface_site_web/components/core_components.ex
@@ -0,0 +1,635 @@
+defmodule SurfaceSiteWeb.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ At the first glance, this module may seem daunting, but its goal is
+ to provide some core building blocks in your application, such as modals,
+ tables, and forms. The components are mostly markup and well documented
+ with doc strings and declarative assigns. You may customize and style
+ them in any way you want, based on your application growth and needs.
+
+ The default components use Tailwind CSS, a utility-first CSS framework.
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
+ how to customize them or feel free to swap in another framework altogether.
+
+ Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+
+ @doc """
+ Renders a modal.
+
+ ## Examples
+
+ <.modal id="confirm-modal">
+ This is a modal.
+
+
+ JS commands may be passed to the `:on_cancel` to configure
+ the closing/cancel event, for example:
+
+ <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
+ This is another modal.
+
+
+ """
+ attr :id, :string, required: true
+ attr :show, :boolean, default: false
+ attr :on_cancel, JS, default: %JS{}
+ slot :inner_block, required: true
+
+ def modal(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Shows the flash group with standard titles and content.
+
+ ## Examples
+
+ <.flash_group flash={@flash} />
+ """
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+
+ def flash_group(assigns) do
+ ~H"""
+ <.flash kind={:info} title="Success!" flash={@flash} />
+ <.flash kind={:error} title="Error!" flash={@flash} />
+ <.flash
+ id="client-error"
+ kind={:error}
+ title="We can't find the internet"
+ phx-disconnected={show(".phx-client-error #client-error")}
+ phx-connected={hide("#client-error")}
+ hidden
+ >
+ Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+
+ <.flash
+ id="server-error"
+ kind={:error}
+ title="Something went wrong!"
+ phx-disconnected={show(".phx-server-error #server-error")}
+ phx-connected={hide("#server-error")}
+ hidden
+ >
+ Hang in there while we get back on track
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+ """
+ end
+
+ @doc """
+ Renders a simple form.
+
+ ## Examples
+
+ <.simple_form for={@form} phx-change="validate" phx-submit="save">
+ <.input field={@form[:email]} label="Email"/>
+ <.input field={@form[:username]} label="Username" />
+ <:actions>
+ <.button>Save
+
+
+ """
+ attr :for, :any, required: true, doc: "the datastructure for the form"
+ attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+
+ attr :rest, :global,
+ include: ~w(autocomplete name rel action enctype method novalidate target multipart),
+ doc: "the arbitrary HTML attributes to apply to the form tag"
+
+ slot :inner_block, required: true
+ slot :actions, doc: "the slot for form actions, such as a submit button"
+
+ def simple_form(assigns) do
+ ~H"""
+ <.form :let={f} for={@for} as={@as} {@rest}>
+
+ <%= render_slot(@inner_block, f) %>
+
+ <%= render_slot(action, f) %>
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a button.
+
+ ## Examples
+
+ <.button>Send!
+ <.button phx-click="go" class="ml-2">Send!
+ """
+ attr :type, :string, default: nil
+ attr :class, :string, default: nil
+ attr :rest, :global, include: ~w(disabled form name value)
+
+ slot :inner_block, required: true
+
+ def button(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Renders an input with label and error messages.
+
+ A `Phoenix.HTML.FormField` may be passed as argument,
+ which is used to retrieve the input name, id, and values.
+ Otherwise all attributes may be passed explicitly.
+
+ ## Types
+
+ This function accepts all HTML input types, considering that:
+
+ * You may also set `type="select"` to render a `