chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
/* !include https://cdn.jsdelivr.net/npm/apexcharts@5.13.0/dist/apexcharts.min.js */
|
||||
|
||||
sqlpage_chart = (() => {
|
||||
function sqlpage_chart() {
|
||||
for (const c of document.querySelectorAll("[data-pre-init=chart]")) {
|
||||
try {
|
||||
build_sqlpage_chart(c);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tblrColors = [
|
||||
["blue", "#1c7ed6", "#339af0"],
|
||||
["red", "#f03e3e", "#ff6b6b"],
|
||||
["green", "#37b24d", "#51cf66"],
|
||||
["pink", "#d6336c", "#f06595"],
|
||||
["purple", "#ae3ec9", "#cc5de8"],
|
||||
["orange", "#f76707", "#ff922b"],
|
||||
["cyan", "#1098ad", "#22b8cf"],
|
||||
["teal", "#0ca678", "#20c997"],
|
||||
["yellow", "#f59f00", "#fcc419"],
|
||||
["indigo", "#4263eb", "#5c7cfa"],
|
||||
["lime", "#74b816", "#94d82d"],
|
||||
["azure", "#339af0", "#339af0"],
|
||||
["gray", "#495057", "#adb5bd"],
|
||||
["black", "#000000", "#000000"],
|
||||
["white", "#ffffff", "#f8f9fa"],
|
||||
];
|
||||
const colorNames = Object.fromEntries(
|
||||
tblrColors.flatMap(([name, dark, light]) => [
|
||||
[name, dark],
|
||||
[`${name}-lt`, light],
|
||||
]),
|
||||
);
|
||||
const isDarkTheme = document.body?.dataset?.bsTheme === "dark";
|
||||
|
||||
/** @typedef { { [name:string]: {data:{x:number|string|Date,y:number}[], name:string} } } Series */
|
||||
|
||||
/**
|
||||
* Aligns series data points by their x-axis categories, ensuring all series have data points
|
||||
* for each unique category. Missing values are filled with zeros.
|
||||
* Categories are ordered by their name.
|
||||
*
|
||||
* @example
|
||||
* // Input series:
|
||||
* const series = [
|
||||
* { name: "A", data: [{x: "X2", y: 10}, {x: "X3", y: 30}] },
|
||||
* { name: "B", data: [{x: "X1", y: 25}, {x: "X2", y: 20}] }
|
||||
* ];
|
||||
*
|
||||
* // Output after align_categories (orderedCategories will be ["X1","X2", "X3"]):
|
||||
* // [
|
||||
* // { name: "A", data: [{x: "X1", y: 0}, {x: "X2", y: 10}, {x: "X3", y: 30}] },
|
||||
* // { name: "B", data: [{x: "X1", y: 25}, {x: "X2", y: 20}, {x: "X3", y: 0}] }
|
||||
* // ]
|
||||
*
|
||||
* @param {(Series[string])[]} series - Array of series objects, each containing name and data points
|
||||
* @returns {Series[string][]} Aligned series with consistent categories across all series
|
||||
*/
|
||||
function align_categories(series) {
|
||||
const categoriesSet = new Set();
|
||||
const pointers = series.map((_) => 0); // Index of current data point in each series
|
||||
const x_at = (series_idx) =>
|
||||
series[series_idx].data[pointers[series_idx]].x;
|
||||
const series_idxs = series.flatMap((s, i) => (s.data.length ? i : []));
|
||||
while (series_idxs.length > 0) {
|
||||
let idx_of_xmin = series_idxs[0];
|
||||
for (const series_idx of series_idxs) {
|
||||
if (x_at(series_idx) < x_at(idx_of_xmin)) idx_of_xmin = series_idx;
|
||||
}
|
||||
|
||||
const new_category = x_at(idx_of_xmin);
|
||||
if (!categoriesSet.has(new_category)) categoriesSet.add(new_category);
|
||||
pointers[idx_of_xmin]++;
|
||||
if (pointers[idx_of_xmin] >= series[idx_of_xmin].data.length) {
|
||||
series_idxs.splice(series_idxs.indexOf(idx_of_xmin), 1);
|
||||
}
|
||||
}
|
||||
// Create a map of category -> value for each series and rebuild
|
||||
return series.map((s) => {
|
||||
const valueMap = new Map(s.data.map((point) => [point.x, point.y]));
|
||||
return {
|
||||
name: s.name,
|
||||
data: Array.from(categoriesSet, (category) => ({
|
||||
x: category,
|
||||
y: valueMap.get(category) || 0,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {HTMLElement} c */
|
||||
function build_sqlpage_chart(c) {
|
||||
const [data_element] = c.getElementsByTagName("data");
|
||||
const data = JSON.parse(data_element.textContent);
|
||||
const chartContainer = c.querySelector(".chart");
|
||||
chartContainer.innerHTML = "";
|
||||
const is_timeseries = !!data.time;
|
||||
/** @type { Series } */
|
||||
const series_map = {};
|
||||
for (const [name, old_x, old_y, z] of data.points) {
|
||||
series_map[name] = series_map[name] || { name, data: [] };
|
||||
let x = old_x;
|
||||
let y = old_y;
|
||||
if (is_timeseries) {
|
||||
if (typeof x === "number") x = new Date(x * 1000);
|
||||
else if (data.type === "rangeBar" && Array.isArray(y))
|
||||
y = y.map((y) => new Date(y).getTime());
|
||||
else x = new Date(x);
|
||||
}
|
||||
series_map[name].data.push({ x, y, z });
|
||||
}
|
||||
if (data.xmin == null) data.xmin = undefined;
|
||||
if (data.xmax == null) data.xmax = undefined;
|
||||
if (data.ymin == null) data.ymin = undefined;
|
||||
if (data.ymax == null) data.ymax = undefined;
|
||||
|
||||
const colors = [
|
||||
...data.colors.filter((c) => c).map((c) => colorNames[c]),
|
||||
...tblrColors.map(([_, dark, light]) => (isDarkTheme ? dark : light)),
|
||||
...tblrColors.map(([_, dark, light]) => (isDarkTheme ? light : dark)),
|
||||
];
|
||||
|
||||
let series = Object.values(series_map);
|
||||
|
||||
let labels;
|
||||
const categories =
|
||||
series.length > 0 && typeof series[0].data[0].x === "string";
|
||||
if (data.type === "pie") {
|
||||
labels = data.points.map(([name, x, _y]) => x || name);
|
||||
series = data.points.map(([_name, _x, y]) => Number.parseFloat(y));
|
||||
} else if (categories && data.type === "bar" && series.length > 1)
|
||||
series = align_categories(series);
|
||||
|
||||
const chart_type = data.type || "line";
|
||||
const options = {
|
||||
chart: {
|
||||
type: chart_type,
|
||||
fontFamily: "inherit",
|
||||
background: "transparent",
|
||||
parentHeightOffset: 0,
|
||||
height: chartContainer.style.height,
|
||||
stacked: !!data.stacked,
|
||||
toolbar: {
|
||||
show: !!data.toolbar,
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
},
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
mode: isDarkTheme ? "dark" : "light",
|
||||
palette: "palette4",
|
||||
},
|
||||
legend: {
|
||||
show: data.show_legend === null || !!data.show_legend,
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: !!data.labels,
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
color: "var(--tblr-primary-bg-subtle)",
|
||||
},
|
||||
formatter:
|
||||
data.type === "rangeBar"
|
||||
? (_val, { seriesIndex, w }) => w.config.series[seriesIndex].name
|
||||
: data.type === "pie"
|
||||
? (value, { seriesIndex, w }) =>
|
||||
`${w.config.labels[seriesIndex]}: ${value.toFixed()}%`
|
||||
: (value) => value?.toLocaleString?.() || value,
|
||||
},
|
||||
fill: {
|
||||
type: data.type === "area" ? "gradient" : "solid",
|
||||
},
|
||||
stroke: {
|
||||
width:
|
||||
{
|
||||
area: 3,
|
||||
line: 2,
|
||||
}[chart_type] || 0,
|
||||
lineCap: "round",
|
||||
curve: "smooth",
|
||||
},
|
||||
xaxis: {
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
min: data.xmin,
|
||||
max: data.xmax,
|
||||
title: {
|
||||
text: data.xtitle || undefined,
|
||||
},
|
||||
type: is_timeseries ? "datetime" : categories ? "category" : undefined,
|
||||
labels: {
|
||||
datetimeUTC: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
logarithmic: !!data.logarithmic,
|
||||
min: data.ymin,
|
||||
max: data.ymax,
|
||||
stepSize: data.ystep,
|
||||
tickAmount: data.yticks,
|
||||
title: {
|
||||
text: data.ytitle || undefined,
|
||||
},
|
||||
},
|
||||
zaxis: {
|
||||
title: {
|
||||
text: data.ztitle || undefined,
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: data.marker || 0,
|
||||
strokeWidth: 0,
|
||||
hover: {
|
||||
sizeOffset: 5,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
fillSeriesColor: false,
|
||||
custom:
|
||||
data.type === "bubble" || data.type === "scatter"
|
||||
? bubbleTooltip
|
||||
: undefined,
|
||||
y: {
|
||||
formatter: (value) => {
|
||||
if (value == null) return "";
|
||||
if (is_timeseries && data.type === "rangeBar") {
|
||||
const d = new Date(value);
|
||||
if (d.getHours() === 0 && d.getMinutes() === 0)
|
||||
return d.toLocaleDateString();
|
||||
return d.toLocaleString();
|
||||
}
|
||||
const str_val = value.toLocaleString();
|
||||
if (str_val.length > 10 && Number.isNaN(value))
|
||||
return value.toFixed(2);
|
||||
return str_val;
|
||||
},
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: !!data.horizontal || data.type === "rangeBar",
|
||||
borderRadius: 5,
|
||||
},
|
||||
bubble: { minBubbleRadius: 5 },
|
||||
},
|
||||
colors,
|
||||
series,
|
||||
};
|
||||
if (labels) options.labels = labels;
|
||||
// tickamount is the number of intervals, not the number of ticks
|
||||
if (data.xticks) options.xaxis.tickAmount = data.xticks;
|
||||
console.log("Rendering chart", options);
|
||||
const chart = new ApexCharts(chartContainer, options);
|
||||
chart.render();
|
||||
if (window.charts) window.charts.push(chart);
|
||||
else window.charts = [chart];
|
||||
c.removeAttribute("data-pre-init");
|
||||
}
|
||||
|
||||
function bubbleTooltip({ seriesIndex, dataPointIndex, w }) {
|
||||
const { name, data } = w.config.series[seriesIndex];
|
||||
const point = data[dataPointIndex];
|
||||
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.className = "apexcharts-tooltip-text";
|
||||
tooltip.style.fontFamily = "inherit";
|
||||
|
||||
const seriesName = document.createElement("div");
|
||||
seriesName.className = "apexcharts-tooltip-y-group";
|
||||
seriesName.style.fontWeight = "bold";
|
||||
seriesName.innerText = name;
|
||||
tooltip.appendChild(seriesName);
|
||||
|
||||
for (const axis of ["x", "y", "z"]) {
|
||||
const value = point[axis];
|
||||
if (value == null) continue;
|
||||
const axisValue = document.createElement("div");
|
||||
axisValue.className = "apexcharts-tooltip-y-group";
|
||||
let axis_conf = w.config[`${axis}axis`];
|
||||
if (axis_conf.length) axis_conf = axis_conf[0];
|
||||
const title = axis_conf.title.text || axis;
|
||||
const labelSpan = document.createElement("span");
|
||||
labelSpan.className = "apexcharts-tooltip-text-y-label";
|
||||
labelSpan.innerText = `${title}: `;
|
||||
axisValue.appendChild(labelSpan);
|
||||
const valueSpan = document.createElement("span");
|
||||
valueSpan.className = "apexcharts-tooltip-text-y-value";
|
||||
valueSpan.innerText = value;
|
||||
axisValue.appendChild(valueSpan);
|
||||
tooltip.appendChild(axisValue);
|
||||
}
|
||||
return tooltip.outerHTML;
|
||||
}
|
||||
|
||||
return sqlpage_chart;
|
||||
})();
|
||||
|
||||
add_init_fn(sqlpage_chart);
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" fill="none" stroke="#e7f6f8" stroke-linecap="round" stroke-linejoin="round" stroke-width="75">
|
||||
<defs>
|
||||
<filter id="b" width="200%" height="200%" x="-50%" y="-50%">
|
||||
<feGaussianBlur stdDeviation="45"/>
|
||||
</filter>
|
||||
<path id="a" d="M171 264a341 122 0 1 0 682 0 341 122 0 1 0-682 0m0 0v245a341 122 0 0 0 682 0V264M171 509v244a341 122 0 0 0 682 0V509"/>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#0f3953" stroke="none" ry="64"/>
|
||||
<use href="#a"/>
|
||||
<use filter="url(#b)" href="#a"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 578 B |
@@ -0,0 +1,47 @@
|
||||
# SQLPage migrations
|
||||
|
||||
SQLPage migrations are SQL scripts that you can use to create or update the database schema.
|
||||
They are entirely optional: you can use SQLPage without them, and manage the database schema yourself with other tools.
|
||||
|
||||
If you are new to SQL migrations, please read our [**introduction to database migrations**](https://sql-page.com/your-first-sql-website/migrations.sql).
|
||||
|
||||
## Creating a migration
|
||||
|
||||
To create a migration, create a file in the `sqlpage/migrations` directory with the following name:
|
||||
|
||||
```
|
||||
<version>_<name>.sql
|
||||
```
|
||||
|
||||
Where `<version>` is a number that represents the version of the migration, and `<name>` is a name for the migration.
|
||||
For example, `001_initial.sql` or `002_add_users.sql`.
|
||||
|
||||
When you need to update the database schema, always create a **new** migration file with a new version number
|
||||
that is greater than the previous one.
|
||||
Use commands like `ALTER TABLE` to update the schema declaratively instead of modifying the existing `CREATE TABLE`
|
||||
statements.
|
||||
|
||||
If you try to edit an existing migration, SQLPage will not run it again, it will detect that the migration has already executed. Also, if the migration is different than the one that was executed, SQLPage will throw an error as the database structure must match.
|
||||
|
||||
## Creating migrations on the command line
|
||||
|
||||
You can create a migration directly with sqlpage by running the command `sqlpage create-migration [migration_name]`
|
||||
|
||||
For example if you run `sqlpage create-migration "Example Migration 1"` on the command line, you will find a new file under the `sqlpage/migrations` folder called `[timestamp]_example_migration_1.sql` where timestamp is the current time when you ran the command.
|
||||
|
||||
## Running migrations
|
||||
|
||||
Migrations that need to be applied are run automatically when SQLPage starts.
|
||||
You need to restart SQLPage each time you create a new migration.
|
||||
|
||||
## How does it work?
|
||||
|
||||
SQLPage keeps track of the migrations that have been applied in a table called `_sqlx_migrations`.
|
||||
This table is created automatically when SQLPage starts for the first time, if you create migration files.
|
||||
If you don't create any migration files, SQLPage will never touch the database schema on its own.
|
||||
|
||||
When SQLPage starts, it checks the `_sqlx_migrations` table to see which migrations have been applied.
|
||||
It checks the `sqlpage/migrations` directory to see which migrations are available.
|
||||
If the checksum of a migration file is different from the checksum of the migration that has been applied,
|
||||
SQLPage will return an error and refuse to start.
|
||||
If you end up in this situation, you can remove the `_sqlx_migrations` table: all your old migrations will be reapplied, and SQLPage will start again.
|
||||
@@ -0,0 +1 @@
|
||||
select 'text' as component, 'private cache bypass secret' as contents;
|
||||
@@ -0,0 +1,193 @@
|
||||
/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css */
|
||||
/* !include https://cdn.jsdelivr.net/npm/tom-select@2.6.1/dist/css/tom-select.bootstrap5.css */
|
||||
/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler-vendors.min.css */
|
||||
|
||||
.navbar {
|
||||
/* https://github.com/sqlpage/SQLPage/issues/822 */
|
||||
--tblr-navbar-color: rgba(var(--tblr-body-color-rgb), 0.8);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .alert:not(.alert-important) {
|
||||
/* See https://github.com/tabler/tabler/issues/1607 */
|
||||
background-color: var(--tblr-bg-surface);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
td > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/** Removes the margin-bottom from the last element */
|
||||
.remove-bottom-margin > :last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.text-secondary a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* orchidjs/tom-select#712 */
|
||||
.ts-wrapper.multi .ts-control > div.active {
|
||||
border: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
/* remove the ugly text highlight in the default tom-select */
|
||||
.ts-dropdown [data-selectable] .highlight {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.page {
|
||||
/* Leave space for the footer */
|
||||
min-height: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
.datagrid {
|
||||
--tblr-datagrid-padding: 1.25rem;
|
||||
--tblr-datagrid-item-width: 6rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.apexcharts-text,
|
||||
.apexcharts-datalabel {
|
||||
fill: var(--tblr-body-color) !important;
|
||||
font-weight: var(--tblr-body-font-weight);
|
||||
}
|
||||
|
||||
/** table **/
|
||||
.table-freeze-headers thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.table-freeze-footers tfoot {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.table-freeze-headers {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.table-freeze-footers {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.table-freeze-columns th:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.table-freeze-columns td:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: var(--tblr-bg-surface-secondary);
|
||||
box-shadow: 3px 0 3px var(--tblr-border-color);
|
||||
}
|
||||
|
||||
/* Prevent the fixed headers from hiding the selected target row */
|
||||
.table-freeze-headers tr[id] {
|
||||
scroll-margin-top: 2.1rem;
|
||||
}
|
||||
|
||||
.article-text {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
font-family: "Times New Roman", serif;
|
||||
width: 65ch;
|
||||
max-width: 100%;
|
||||
margin: 1.5em auto;
|
||||
}
|
||||
|
||||
.article-text p::first-letter {
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
li p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
background: var(--tblr-active-bg) !important;
|
||||
}
|
||||
|
||||
/*
|
||||
See https://github.com/tabler/tabler/issues/2404
|
||||
*/
|
||||
.status-x {
|
||||
--tblr-status-color: #000000;
|
||||
--tblr-status-color-rgb: 0, 0, 0;
|
||||
}
|
||||
.status-facebook {
|
||||
--tblr-status-color: #1877f2;
|
||||
--tblr-status-color-rgb: 24, 119, 242;
|
||||
}
|
||||
.status-twitter {
|
||||
--tblr-status-color: #1da1f2;
|
||||
--tblr-status-color-rgb: 29, 161, 242;
|
||||
}
|
||||
.status-linkedin {
|
||||
--tblr-status-color: #0a66c2;
|
||||
--tblr-status-color-rgb: 10, 102, 194;
|
||||
}
|
||||
.status-google {
|
||||
--tblr-status-color: #dc4e41;
|
||||
--tblr-status-color-rgb: 220, 78, 65;
|
||||
}
|
||||
.status-youtube {
|
||||
--tblr-status-color: #ff0000;
|
||||
--tblr-status-color-rgb: 255, 0, 0;
|
||||
}
|
||||
.status-vimeo {
|
||||
--tblr-status-color: #1ab7ea;
|
||||
--tblr-status-color-rgb: 26, 183, 234;
|
||||
}
|
||||
.status-dribbble {
|
||||
--tblr-status-color: #ea4c89;
|
||||
--tblr-status-color-rgb: 234, 76, 137;
|
||||
}
|
||||
.status-github {
|
||||
--tblr-status-color: #181717;
|
||||
--tblr-status-color-rgb: 24, 23, 23;
|
||||
}
|
||||
.status-instagram {
|
||||
--tblr-status-color: #e4405f;
|
||||
--tblr-status-color-rgb: 228, 64, 95;
|
||||
}
|
||||
.status-pinterest {
|
||||
--tblr-status-color: #bd081c;
|
||||
--tblr-status-color-rgb: 189, 8, 28;
|
||||
}
|
||||
.status-vk {
|
||||
--tblr-status-color: #6383a8;
|
||||
--tblr-status-color-rgb: 99, 131, 168;
|
||||
}
|
||||
.status-rss {
|
||||
--tblr-status-color: #ffa500;
|
||||
--tblr-status-color-rgb: 255, 165, 0;
|
||||
}
|
||||
.status-flickr {
|
||||
--tblr-status-color: #0063dc;
|
||||
--tblr-status-color-rgb: 0, 99, 220;
|
||||
}
|
||||
.status-bitbucket {
|
||||
--tblr-status-color: #0052cc;
|
||||
--tblr-status-color-rgb: 0, 82, 204;
|
||||
}
|
||||
.status-tabler {
|
||||
--tblr-status-color: #066fd1;
|
||||
--tblr-status-color-rgb: 6, 111, 209;
|
||||
}
|
||||
|
||||
.text-black-fg {
|
||||
color: var(--tblr-dark-fg) !important;
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/js/tabler.min.js */
|
||||
const nonce = document.currentScript.nonce;
|
||||
|
||||
function sqlpage_card() {
|
||||
for (const c of document.querySelectorAll("[data-pre-init=card]")) {
|
||||
c.removeAttribute("data-pre-init");
|
||||
const url = new URL(c.dataset.embed, window.location.href);
|
||||
url.searchParams.set("_sqlpage_embed", "1");
|
||||
fetch(url)
|
||||
.then((res) => res.text())
|
||||
.then((html) => {
|
||||
const body = c.querySelector(".card-content");
|
||||
body.innerHTML = html;
|
||||
const spinner = c.querySelector(".card-loading-placeholder");
|
||||
if (spinner) {
|
||||
spinner.parentNode.removeChild(spinner);
|
||||
}
|
||||
const fragLoadedEvt = new CustomEvent("fragment-loaded", {
|
||||
bubbles: true,
|
||||
});
|
||||
c.dispatchEvent(fragLoadedEvt);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {HTMLElement} root_el */
|
||||
function setup_table(root_el) {
|
||||
/** @type {HTMLInputElement | null} */
|
||||
const search_input = root_el.querySelector("input.search");
|
||||
const table_el = root_el.querySelector("table");
|
||||
const sort_buttons = [...table_el.querySelectorAll("button.sort[data-sort]")];
|
||||
const item_parent = table_el.querySelector("tbody");
|
||||
const has_sort = sort_buttons.length > 0;
|
||||
|
||||
if (search_input || has_sort) {
|
||||
const items = table_parse_data(table_el, sort_buttons);
|
||||
if (search_input) setup_table_search_behavior(search_input, items);
|
||||
if (has_sort) setup_sort_behavior(sort_buttons, items, item_parent);
|
||||
}
|
||||
|
||||
// Change number format AFTER parsing and storing the sort keys
|
||||
apply_number_formatting(table_el);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLInputElement} search_input
|
||||
* @param {Array<{el: HTMLElement, sort_keys: Array<{num: number, str: string}>}>} items
|
||||
*/
|
||||
function setup_table_search_behavior(search_input, items) {
|
||||
function onSearch() {
|
||||
const lower_search = search_input.value
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((s) => s);
|
||||
for (const item of items) {
|
||||
const show = lower_search.every((s) =>
|
||||
item.el.textContent.toLowerCase().includes(s),
|
||||
);
|
||||
item.el.style.display = show ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
search_input.addEventListener("input", onSearch);
|
||||
onSearch();
|
||||
}
|
||||
|
||||
/**@param {HTMLElement} table_el */
|
||||
function apply_number_formatting(table_el) {
|
||||
const header_els = table_el.querySelectorAll("thead > tr > th");
|
||||
const col_types = [...header_els].map((el) => el.dataset.column_type);
|
||||
const col_rawnums = [...header_els].map((el) => !!el.dataset.raw_number);
|
||||
const col_money = [...header_els].map((el) => !!el.dataset.money);
|
||||
const number_format_locale = table_el.dataset.number_format_locale;
|
||||
const number_format_digits = table_el.dataset.number_format_digits;
|
||||
const currency = table_el.dataset.currency;
|
||||
|
||||
for (const tr_el of table_el.querySelectorAll("tbody tr, tfoot tr")) {
|
||||
const cells = tr_el.getElementsByTagName("td");
|
||||
for (let idx = 0; idx < cells.length; idx++) {
|
||||
const column_type = col_types[idx];
|
||||
const is_raw_number = col_rawnums[idx];
|
||||
const cell_el = cells[idx];
|
||||
const text = cell_el.textContent;
|
||||
|
||||
if (column_type === "number" && !is_raw_number && text) {
|
||||
const num = Number.parseFloat(text);
|
||||
const is_money = col_money[idx];
|
||||
cell_el.textContent = num.toLocaleString(number_format_locale, {
|
||||
maximumFractionDigits: number_format_digits,
|
||||
currency,
|
||||
style: is_money ? "currency" : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Prepare the table rows for sorting.
|
||||
* @param {HTMLElement} table_el
|
||||
* @param {HTMLElement[]} sort_buttons
|
||||
*/
|
||||
function table_parse_data(table_el, sort_buttons) {
|
||||
const is_num = [...sort_buttons].map(
|
||||
(btn_el) => btn_el.parentElement.dataset.column_type === "number",
|
||||
);
|
||||
return [...table_el.querySelectorAll("tbody tr")].map((tr_el) => {
|
||||
const cells = tr_el.getElementsByTagName("td");
|
||||
return {
|
||||
el: tr_el,
|
||||
sort_keys: sort_buttons.map((_btn_el, idx) => {
|
||||
const str = cells[idx]?.textContent;
|
||||
const num = is_num[idx] ? Number.parseFloat(str) : Number.NaN;
|
||||
return { num, str };
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event listeners to the sort buttons to sort the table rows.
|
||||
* @param {HTMLElement[]} sort_buttons
|
||||
* @param {HTMLElement[]} items
|
||||
* @param {HTMLElement} item_parent
|
||||
*/
|
||||
function setup_sort_behavior(sort_buttons, items, item_parent) {
|
||||
sort_buttons.forEach((button, button_index) => {
|
||||
button.addEventListener("click", function sort_items() {
|
||||
const sort_desc = button.classList.contains("asc");
|
||||
for (const b of sort_buttons) {
|
||||
b.classList.remove("asc", "desc");
|
||||
}
|
||||
button.classList.add(sort_desc ? "desc" : "asc");
|
||||
const multiplier = sort_desc ? -1 : 1;
|
||||
items.sort((a, b) => {
|
||||
const a_key = a.sort_keys[button_index];
|
||||
const b_key = b.sort_keys[button_index];
|
||||
return (
|
||||
multiplier *
|
||||
(Number.isNaN(a_key.num) || Number.isNaN(b_key.num)
|
||||
? a_key.str.localeCompare(b_key.str)
|
||||
: a_key.num - b_key.num)
|
||||
);
|
||||
});
|
||||
item_parent.append(...items.map((item) => item.el));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sqlpage_table() {
|
||||
for (const r of document.querySelectorAll("[data-pre-init=table]")) {
|
||||
r.removeAttribute("data-pre-init");
|
||||
setup_table(r);
|
||||
}
|
||||
}
|
||||
|
||||
let is_leaflet_injected = false;
|
||||
let is_leaflet_loaded = false;
|
||||
|
||||
function sqlpage_map() {
|
||||
const first_map = document.querySelector("[data-pre-init=map]");
|
||||
const leaflet_base_url = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4";
|
||||
if (first_map && !is_leaflet_injected) {
|
||||
// Add the leaflet js and css to the page
|
||||
const leaflet_css = document.createElement("link");
|
||||
leaflet_css.rel = "stylesheet";
|
||||
leaflet_css.href = `${leaflet_base_url}/dist/leaflet.css`;
|
||||
leaflet_css.integrity =
|
||||
"sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
|
||||
leaflet_css.crossOrigin = "anonymous";
|
||||
document.head.appendChild(leaflet_css);
|
||||
const leaflet_js = document.createElement("script");
|
||||
leaflet_js.src = `${leaflet_base_url}/dist/leaflet.js`;
|
||||
leaflet_js.integrity =
|
||||
"sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
|
||||
leaflet_js.crossOrigin = "anonymous";
|
||||
leaflet_js.nonce = nonce;
|
||||
leaflet_js.onload = onLeafletLoad;
|
||||
document.head.appendChild(leaflet_js);
|
||||
is_leaflet_injected = true;
|
||||
}
|
||||
if (first_map && is_leaflet_loaded) {
|
||||
onLeafletLoad();
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {string|undefined} coords
|
||||
* @returns {[number, number] | undefined}
|
||||
*/
|
||||
function parseCoords(coords) {
|
||||
return coords?.split(",", 2).map((c) => Number.parseFloat(c));
|
||||
}
|
||||
function onLeafletLoad() {
|
||||
is_leaflet_loaded = true;
|
||||
const maps = document.querySelectorAll("[data-pre-init=map]");
|
||||
for (const m of maps) {
|
||||
const tile_source = m.dataset.tile_source;
|
||||
const maxZoom = +m.dataset.max_zoom;
|
||||
const attribution = m.dataset.attribution;
|
||||
const map = L.map(m, { attributionControl: !!attribution });
|
||||
const zoom = m.dataset.zoom;
|
||||
const center = parseCoords(m.dataset.center);
|
||||
if (tile_source)
|
||||
L.tileLayer(tile_source, { attribution, maxZoom }).addTo(map);
|
||||
map._sqlpage_markers = [];
|
||||
for (const marker_elem of m.getElementsByClassName("marker")) {
|
||||
setTimeout(addMarker, 0, marker_elem, map);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (center) map.setView(center, +zoom);
|
||||
else {
|
||||
const markerBounds = (m) =>
|
||||
m.getLatLng ? m.getLatLng() : m.getBounds();
|
||||
const bounds = map._sqlpage_markers.map(markerBounds);
|
||||
if (bounds.length > 0) map.fitBounds(bounds);
|
||||
else map.setView([51.505, 10], +zoom);
|
||||
if (zoom != null) map.setZoom(+zoom);
|
||||
}
|
||||
}, 100);
|
||||
m.removeAttribute("data-pre-init");
|
||||
m.getElementsByClassName("spinner-border")[0]?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function addMarker(marker_elem, map) {
|
||||
const { dataset } = marker_elem;
|
||||
const options = {
|
||||
color: marker_elem.dataset.color,
|
||||
title: marker_elem.getElementsByTagName("h3")[0].textContent.trim(),
|
||||
};
|
||||
const marker = dataset.coords
|
||||
? createMarker(marker_elem, options)
|
||||
: createGeoJSONMarker(marker_elem, options);
|
||||
marker.addTo(map);
|
||||
map._sqlpage_markers.push(marker);
|
||||
if (marker_elem.textContent.trim()) marker.bindPopup(marker_elem);
|
||||
else if (marker_elem.dataset.link) {
|
||||
marker.on("click", () => {
|
||||
window.location.href = marker_elem.dataset.link;
|
||||
});
|
||||
}
|
||||
}
|
||||
function createMarker(marker_elem, options) {
|
||||
const coords = parseCoords(marker_elem.dataset.coords);
|
||||
const icon_obj = marker_elem.getElementsByClassName("mapicon")[0];
|
||||
if (icon_obj) {
|
||||
const size =
|
||||
1.5 *
|
||||
+(options.size || icon_obj.firstChild?.getAttribute("width") || 24);
|
||||
options.icon = L.divIcon({
|
||||
html: icon_obj,
|
||||
className: `border-0 bg-${options.color || "primary"} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`,
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2],
|
||||
});
|
||||
}
|
||||
return L.marker(coords, options);
|
||||
}
|
||||
function createGeoJSONMarker(marker_elem, options) {
|
||||
const geojson = JSON.parse(marker_elem.dataset.geojson);
|
||||
if (options.color) {
|
||||
options.color = get_tabler_color(options.color) || options.color;
|
||||
}
|
||||
function style({ properties }) {
|
||||
if (typeof properties !== "object") return options;
|
||||
return { ...options, ...properties };
|
||||
}
|
||||
function pointToLayer(feature, latlng) {
|
||||
marker_elem.dataset.coords = `${latlng.lat},${latlng.lng}`;
|
||||
return createMarker(marker_elem, { ...options, ...feature.properties });
|
||||
}
|
||||
return L.geoJSON(geojson, { style, pointToLayer });
|
||||
}
|
||||
}
|
||||
|
||||
function sqlpage_form() {
|
||||
const file_inputs = document.querySelectorAll(
|
||||
"input[type=file][data-max-size]",
|
||||
);
|
||||
for (const input of file_inputs) {
|
||||
const max_size = +input.dataset.maxSize;
|
||||
input.addEventListener("change", function () {
|
||||
input.classList.remove("is-invalid");
|
||||
input.setCustomValidity("");
|
||||
for (const { size } of this.files) {
|
||||
if (size > max_size) {
|
||||
input.classList.add("is-invalid");
|
||||
return input.setCustomValidity(
|
||||
`File size must be less than ${max_size / 1000} kB.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const auto_submit_forms = document.querySelectorAll("form[data-auto-submit]");
|
||||
for (const form of auto_submit_forms) {
|
||||
form.addEventListener("change", () => form.submit());
|
||||
}
|
||||
}
|
||||
|
||||
function get_tabler_color(name) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(
|
||||
`--tblr-${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
function load_scripts() {
|
||||
const addjs = document.querySelectorAll("[data-sqlpage-js]");
|
||||
const existing_scripts = new Set(
|
||||
[...document.querySelectorAll("script")].map((s) => s.src),
|
||||
);
|
||||
for (const el of addjs) {
|
||||
const js = new URL(el.dataset.sqlpageJs, window.location.href).href;
|
||||
if (existing_scripts.has(js)) continue;
|
||||
existing_scripts.add(js);
|
||||
const script = document.createElement("script");
|
||||
script.src = js;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
|
||||
function add_init_fn(f) {
|
||||
document.addEventListener("DOMContentLoaded", f);
|
||||
document.addEventListener("fragment-loaded", f);
|
||||
if (document.readyState !== "loading") setTimeout(f, 0);
|
||||
}
|
||||
|
||||
add_init_fn(sqlpage_table);
|
||||
add_init_fn(sqlpage_map);
|
||||
add_init_fn(sqlpage_card);
|
||||
add_init_fn(sqlpage_form);
|
||||
add_init_fn(load_scripts);
|
||||
|
||||
function init_bootstrap_components(event) {
|
||||
const bootstrap = window.bootstrap || window.tabler.bootstrap;
|
||||
const fragment = event.target;
|
||||
for (const el of fragment.querySelectorAll('[data-bs-toggle="tooltip"]')) {
|
||||
new bootstrap.Tooltip(el);
|
||||
}
|
||||
for (const el of fragment.querySelectorAll('[data-bs-toggle="popover"]')) {
|
||||
new bootstrap.Popover(el);
|
||||
}
|
||||
for (const el of fragment.querySelectorAll('[data-bs-toggle="dropdown"]')) {
|
||||
new bootstrap.Dropdown(el);
|
||||
}
|
||||
for (const el of fragment.querySelectorAll('[data-bs-ride="carousel"]')) {
|
||||
new bootstrap.Carousel(el);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("fragment-loaded", init_bootstrap_components);
|
||||
|
||||
function open_modal_for_hash() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (!hash) return;
|
||||
const modal = document.getElementById(hash);
|
||||
if (!modal || !modal.classList.contains("modal")) return;
|
||||
const bootstrap_modal =
|
||||
window.tabler.bootstrap.Modal.getOrCreateInstance(modal);
|
||||
bootstrap_modal.show();
|
||||
modal.addEventListener("hidden.bs.modal", () => {
|
||||
window.history.replaceState(null, "", "#");
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", open_modal_for_hash);
|
||||
window.addEventListener("DOMContentLoaded", open_modal_for_hash);
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"database_url": "sqlite://./sqlpage/sqlpage.db?mode=rwc"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/* !include https://cdn.jsdelivr.net/npm/@tabler/icons-sprite@3.44.0/dist/tabler-sprite.svg */
|
||||
@@ -0,0 +1,20 @@
|
||||
# SQLPage component templates
|
||||
|
||||
SQLPage templates are handlebars[^1] files that are used to render the results of SQL queries.
|
||||
|
||||
[^1]: https://handlebarsjs.com/
|
||||
|
||||
## Default components
|
||||
|
||||
SQLPage comes with a set of default[^2] components that you can use without having to write any code.
|
||||
These are documented on https://sql-page.com/components.sql
|
||||
|
||||
## Custom components
|
||||
|
||||
You can [write your own component templates](https://sql-page.com/custom_components.sql)
|
||||
and place them in the `sqlpage/templates` directory.
|
||||
To override a default component, create a file with the same name as the default component.
|
||||
If you want to start from an existing component, you can copy it from the `sqlpage/templates` directory
|
||||
in the SQLPage source code[^2].
|
||||
|
||||
[^2]: A simple component to start from: https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/code.handlebars
|
||||
@@ -0,0 +1,41 @@
|
||||
<div {{~#if id}} id="{{id}}" {{/if}} class="alert alert-{{default color " info"}} {{class}} {{~#if dismissible}}
|
||||
alert-dismissible{{/if~}} {{~#if important}} alert-important{{/if~}} " role=" alert">
|
||||
|
||||
{{~#if icon~}}
|
||||
<div class="icon alert-icon">
|
||||
{{~icon_img icon~}}
|
||||
</div>
|
||||
{{~/if~}}
|
||||
|
||||
<div class="overflow-auto">
|
||||
{{~#if title~}}
|
||||
<h4 class="alert-title" {{~#if important~}}style="color:inherit" {{/if~}}>{{title}}</h4>
|
||||
{{~/if~}}
|
||||
|
||||
{{~#if description~}}
|
||||
<div class="alert-description">
|
||||
{{~description~}}
|
||||
</div>
|
||||
{{~/if~}}
|
||||
|
||||
{{~#if description_md~}}
|
||||
<div class="alert-description" style="margin-bottom: -1rem;">
|
||||
{{{~markdown description_md~}}}
|
||||
</div>
|
||||
{{~/if~}}
|
||||
|
||||
{{~#if link~}}
|
||||
<a href="{{link}}" class="btn btn-sm alert-link mt-2 px-2 {{~#if important~}}text-{{default color 'info'}}{{/if~}}">
|
||||
{{default link_text "Ok"}}
|
||||
</a>
|
||||
{{~/if~}}
|
||||
|
||||
{{~#each_row~}}
|
||||
<a href="{{link}}" class="btn btn-sm btn-{{default color 'primary'}} mt-2 mx-1 px-2">{{default title "Ok"}}</a>
|
||||
{{~/each_row~}}
|
||||
</div>
|
||||
|
||||
{{~#if dismissible~}}
|
||||
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
|
||||
{{~/if~}}
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
<div {{#if id}} id="{{id}}" {{/if~}}
|
||||
class="row g-3 h-100 mb-2
|
||||
{{~#if columns}} row-cols-1 row-cols-sm-2 row-cols-lg-{{columns~}} {{/if~}}
|
||||
{{~#if class}} {{class}} {{/if}}
|
||||
">
|
||||
{{#each_row}}
|
||||
<div class="col {{~#if class}} {{class}}{{/if}}"
|
||||
{{#unless ../columns}}style="min-width: 9rem;"{{/unless}}
|
||||
{{~#if id}} id="{{id}}"{{/if~}}
|
||||
>
|
||||
<div class="card flex-fill {{#if color}}bg-{{color}}-lt{{/if}}">
|
||||
<div class="card-body d-flex flex-column">
|
||||
|
||||
{{#if title}}
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader text-truncate me-2">
|
||||
{{#if title_link}}
|
||||
<a href="{{title_link}}" class="text-decoration-none"
|
||||
{{#if title_link_new_tab}}
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
{{/if}}
|
||||
>
|
||||
{{title}}
|
||||
</a>
|
||||
{{else}}
|
||||
{{title}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if dropdown_item}}
|
||||
<div class="ms-auto lh-1">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-toggle text-secondary" href="#" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{dropdown_item.[0].label}}</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
{{#each dropdown_item}}
|
||||
{{#if link}}
|
||||
<a class="dropdown-item {{#if active}}active{{/if}}" href="{{link}}">{{label}}</a>
|
||||
{{else}}
|
||||
<span class="dropdown-item {{#if active}}active{{/if}}">{{label}}</span>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="d-flex align-items-center mt-1">
|
||||
<div class="h1 {{#if description}}mb-3{{else}}mb-0{{/if}} mt-auto text-nowrap text-truncate">
|
||||
{{#if value_link}}
|
||||
<a href="{{value_link}}"
|
||||
class="text-decoration-none"
|
||||
{{#if value_link_new_tab}} target="_blank" rel="noopener noreferrer"
|
||||
{{/if}}
|
||||
>
|
||||
{{value}}{{#if unit}} {{unit}}{{/if}}
|
||||
</a>
|
||||
{{else}}
|
||||
{{value}}{{#if unit}} {{unit}}{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if (and change_percent (not description))}}
|
||||
<div class="ms-auto">
|
||||
{{#if change_percent}}
|
||||
<span class="text-{{#if (gte change_percent 0)}}success{{else}}danger{{/if}} d-inline-flex align-items-center lh-1">
|
||||
{{change_percent}}%
|
||||
{{#if (gte change_percent 0)}}
|
||||
{{~icon_img 'trending-up'~}}
|
||||
{{else}}
|
||||
{{~icon_img 'trending-down'~}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if description}}
|
||||
<div class="d-flex flex-wrap mb-2" title="{{description}}">
|
||||
<div class="text-truncate me-2">{{description}}</div>
|
||||
{{#if change_percent}}
|
||||
<div class="ms-auto">
|
||||
<span class="text-{{#if (gte change_percent 0)}}success{{else}}danger{{/if}} d-inline-flex align-items-center lh-1">
|
||||
{{change_percent}}%
|
||||
{{#if (gte change_percent 0)}}
|
||||
{{~icon_img 'trending-up'~}}
|
||||
{{else}}
|
||||
{{~icon_img 'trending-down'~}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if progress_percent}}
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-{{progress_color}}" style="width: {{progress_percent}}%" role="progressbar" aria-valuenow="{{progress_percent}}" aria-valuemin="0" aria-valuemax="100" aria-label="{{progress_percent}}% Complete">
|
||||
<span class="visually-hidden">{{progress_percent}}% Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each_row}}
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<nav {{#if id}}id="{{id}}"{{/if}} aria-label="breadcrumb" class="my-1 {{class}}">
|
||||
<ol class="breadcrumb">
|
||||
{{#each_row}}
|
||||
<li class="breadcrumb-item{{#if active}} active{{/if}}" {{#if active}}aria-current="page"{{/if}}>
|
||||
<a href="
|
||||
{{~#if link~}}
|
||||
{{link}}
|
||||
{{~else~}}
|
||||
?link={{title}}
|
||||
{{~/if~}}"
|
||||
{{~#if description}} title="{{description}}"{{/if~}}
|
||||
>{{title}}</a>
|
||||
</li>
|
||||
{{/each_row}}
|
||||
</ol>
|
||||
</nav>
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="btn-list mb-2 {{#if justify}}justify-content-{{justify}}{{/if}} {{class}}">
|
||||
{{#each_row}}
|
||||
{{#if form}}
|
||||
<button type="submit" form="{{form}}" {{#if link}}formaction="{{link}}"{{/if}}
|
||||
{{else}}
|
||||
<a href="{{link}}"
|
||||
{{/if}}
|
||||
class="btn text-wrap{{#if disabled}} disabled{{/if}}
|
||||
{{~#if color}} btn-{{color}}{{/if}}
|
||||
{{~#if ../size}} btn-{{../size}}{{/if}}
|
||||
{{~#if outline}} btn-outline-{{outline}}{{/if}}
|
||||
{{~#if ../shape}} btn-{{../shape}}{{/if}}
|
||||
{{~#if narrow}} btn-icon{{/if}}
|
||||
{{~#if space_after}} me-auto{{/if}}"
|
||||
{{~#if id}} id="{{id}}"{{/if}}
|
||||
{{~#if target}} target="{{target}}"{{/if}}
|
||||
{{~#if download}} download="{{download}}"{{/if}}
|
||||
{{~#if rel}} rel="{{rel}}"{{/if}}
|
||||
{{~#if tooltip}} data-bs-toggle="tooltip" data-bs-placement="top" title="{{tooltip}}"{{/if}}
|
||||
{{~#if modal}} data-bs-toggle="modal" data-bs-target="#{{modal}}"{{/if}}
|
||||
role="button">
|
||||
{{~#if image~}}
|
||||
<span {{~#if title}} class="me-1"{{/if}}>
|
||||
{{~#if (eq ../size 'sm')}}
|
||||
<img width=16 height=16 src="{{image}}">
|
||||
{{~else~}}
|
||||
<img width=24 height=24 src="{{image}}">
|
||||
{{~/if~}}
|
||||
</span>
|
||||
{{~/if~}}
|
||||
{{~#if icon~}}
|
||||
<span {{~#if (not narrow)}} class="me-1"{{/if}}>{{~icon_img icon~}}</span>
|
||||
{{~/if~}}
|
||||
{{~title~}}
|
||||
{{~#if icon_after ~}}
|
||||
<span class="ms-1">{{~icon_img icon_after~}}</span>
|
||||
{{~/if}}
|
||||
{{#if form}}
|
||||
</button>
|
||||
{{else}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{/each_row}}
|
||||
</div>
|
||||
@@ -0,0 +1,100 @@
|
||||
{{#if title}}
|
||||
<h2 class="mt-3 mb-0">{{title}}</h2>
|
||||
{{/if}}
|
||||
{{#if description}}
|
||||
<p>{{description}}</p>
|
||||
{{/if}}
|
||||
{{#if description_md}}
|
||||
{{{markdown description_md}}}
|
||||
{{/if}}
|
||||
<div class="row gx-2 gy-2 mt-1 mb-3 {{class}}
|
||||
{{~#if columns~}}
|
||||
{{#if (gt columns 2)}} row-cols-sm-2 {{/if}}
|
||||
row-cols-lg-{{columns}}
|
||||
{{/if}}">
|
||||
{{#each_row}}
|
||||
<div class="
|
||||
{{#unless ../columns}}
|
||||
{{~#if width~}}
|
||||
{{~#if (lt width 6)}} col-12 col-md-6 {{/if~}}
|
||||
col-lg-{{width~}}
|
||||
{{~else~}}
|
||||
col-12 col-md-6 col-lg-3
|
||||
{{~/if~}}
|
||||
{{/unless}} {{class}}"
|
||||
{{~#if id}} id="{{id}}"{{/if}}>
|
||||
<div class="card h-100
|
||||
{{~#if active}} card-active {{/if~}}
|
||||
{{~#if background_color}}
|
||||
bg-{{background_color}} text-{{background_color}}-fg
|
||||
{{~else}}
|
||||
{{#if (all embed (not title) (not icon))}}
|
||||
bg-transparent border-0
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
"
|
||||
{{#if (and embed (ne embed_mode "iframe"))}}data-pre-init="card" data-embed="{{embed}}"{{/if}}>
|
||||
{{#if link}}
|
||||
<a href="{{link}}" style="text-decoration: inherit; color: inherit">
|
||||
{{/if}}
|
||||
{{#if top_image}}
|
||||
<img src="{{top_image}}" class="card-img-top"{{#if top_image_width}} width="{{top_image_width}}"{{/if}}{{#if top_image_height}} height="{{top_image_height}}"{{/if}}{{#if top_image_lazy}} loading="lazy"{{/if}}/>
|
||||
{{/if}}
|
||||
{{#if color}}
|
||||
{{#if (not embed)}}
|
||||
<div class="card-status-start bg-{{color}}"></div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="card-body {{#if (all embed (not title) (not icon))}}p-0{{/if}}">
|
||||
{{#if title}}<h2 class="card-title fs-3 me-3">{{title}}</h2>{{/if}}
|
||||
<div class="card-content remove-bottom-margin{{#if (and icon (not title))}} pe-4{{/if}}">
|
||||
{{~description~}}
|
||||
{{~#if description_md~}}
|
||||
{{{markdown description_md}}}
|
||||
{{~/if~}}
|
||||
</div>
|
||||
{{~#if embed ~}}
|
||||
{{~#if (eq embed_mode "iframe")}}
|
||||
<iframe src="{{embed}}"
|
||||
width="{{#if width}}{{width}}{{else}}100%{{/if}}"
|
||||
{{~#if height}} height="{{height}}"{{/if~}}
|
||||
{{~#if allow}} allow="{{allow}}"{{/if~}}
|
||||
{{~#if sandbox}} sandbox="{{sandbox}}"{{/if~}}
|
||||
{{~#if style}} style="{{style}}"{{/if~}}
|
||||
>
|
||||
</iframe>
|
||||
{{~else~}}
|
||||
<div class="d-flex justify-content-center h-100 align-items-center card-loading-placeholder">
|
||||
<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
{{~/if~}}
|
||||
{{~/if~}}
|
||||
</div>
|
||||
{{#if link}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if footer}}
|
||||
<div class="card-footer text-muted py-2">
|
||||
{{#if footer_link}}
|
||||
<a href="{{footer_link}}">{{footer}}</a>
|
||||
{{else}}
|
||||
{{footer}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if footer_md}}
|
||||
<div class="card-footer text-muted py-2 remove-bottom-margin">
|
||||
{{{markdown footer_md}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
<div class="ribbon bg-{{color}} fs-2">
|
||||
{{~icon_img icon~}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each_row}}
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<div class="card my-2 col-md-{{default width 12}}{{#if center}} mx-auto{{/if}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<div class="card-body">
|
||||
{{#if title}}
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">{{title}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div id="_sqlpage_carousel_{{@component_index}}" class="carousel slide{{#if fade}} carousel-fade{{/if}}" {{#if auto}}data-bs-ride="carousel"{{/if}} {{#if delay}}data-bs-interval="{{delay}}"{{/if}}>
|
||||
<div class="carousel-indicators{{#if indicators}} carousel-indicators-{{indicators}}{{/if}}{{#if vertical}} carousel-indicators-vertical{{/if}}">
|
||||
{{#each_row}}
|
||||
<button type="button" data-bs-target="#_sqlpage_carousel_{{@../component_index}}" data-bs-slide-to="{{@row_index}}" {{#if (eq @row_index 0)}}class="active"{{/if}}></button>
|
||||
{{#delay}}
|
||||
{{flush_delayed}}
|
||||
<div class="carousel-item {{#if (eq @row_index 0)}}active{{/if}}">
|
||||
<img class="d-block w-100 object-fit-cover" alt="{{image}}" src="{{image}}" {{#if width}}width="{{width}}"{{/if}} {{#if height}}height="{{height}}"{{/if}} />
|
||||
{{#if title}}
|
||||
<div class="carousel-caption-background d-none d-md-block"></div>
|
||||
<div class="carousel-caption d-none d-md-block">
|
||||
<h5>{{title}}</h5>
|
||||
{{#if description_md}}<p>{{{markdown description_md}}}</p>{{else}}{{#if description}}<p>{{description}}</p>{{/if}}{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/delay}}
|
||||
{{/each_row}}
|
||||
</div>
|
||||
<div class="carousel-inner">{{flush_delayed}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if controls}}
|
||||
<a class="carousel-control-prev" data-bs-target="#_sqlpage_carousel_{{@component_index}}" role="button" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" data-bs-target="#_sqlpage_carousel_{{@component_index}}" role="button" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
<div
|
||||
{{#if id}}id="{{id}}"{{/if}}
|
||||
class="card my-2 {{class}}"
|
||||
data-pre-init="chart"
|
||||
data-sqlpage-js="{{static_path 'apexcharts.js'}}"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="d-flex">
|
||||
<h3 class="card-title">{{title}}</h3>
|
||||
</div>
|
||||
<div class="chart" style="height: {{default height 250}}px;">
|
||||
<div class="d-flex justify-content-center h-100 align-items-center">
|
||||
<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<data hidden>
|
||||
{
|
||||
"type": {{stringify type}},
|
||||
"time": {{stringify time}},
|
||||
"labels": {{stringify labels}},
|
||||
"marker": {{stringify marker}},
|
||||
"xtitle": {{stringify xtitle}},
|
||||
"ytitle": {{stringify ytitle}},
|
||||
"ztitle": {{stringify ztitle}},
|
||||
"xticks": {{stringify xticks}},
|
||||
"yticks": {{stringify yticks}},
|
||||
"ystep": {{stringify ystep}},
|
||||
"xmin": {{stringify xmin}},
|
||||
"ymin": {{stringify ymin}},
|
||||
"xmax": {{stringify xmax}},
|
||||
"ymax": {{stringify ymax}},
|
||||
"toolbar": {{stringify toolbar}},
|
||||
"show_legend": {{stringify show_legend}},
|
||||
"logarithmic": {{stringify logarithmic}},
|
||||
"horizontal": {{stringify horizontal}},
|
||||
"stacked": {{stringify stacked}},
|
||||
"height": {{stringify (default height 250)}},
|
||||
"colors": {{stringify (to_array color)}},
|
||||
"points": [
|
||||
{{~#each_row~}}
|
||||
{{~#if (gt @row_index 0)}},{{/if~}}
|
||||
[
|
||||
{{~ stringify (default series (default ../title "")) ~}},
|
||||
{{~ stringify (default x label) ~}},
|
||||
{{~ stringify (default y value) ~}}
|
||||
{{~#if z}}, {{~ stringify z ~}} {{~/if~}}
|
||||
]
|
||||
{{~/each_row~}}
|
||||
]
|
||||
}
|
||||
</data>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="code-component my-1 {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
{{#each_row}}
|
||||
{{#if title}}<h2>{{title}}</h2>{{/if}}
|
||||
{{#if description}}<p>{{description}}</p>{{else}}{{#if description_md}}{{{markdown description_md}}}{{/if}}{{/if}}
|
||||
<pre class="mb-0"><code {{#if language}}class="language-{{language}}"{{/if}}>{{contents}}</code></pre>
|
||||
{{/each_row}}
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<div class="row row-cards d-flex align-items-stretch justify-content-center my-3">
|
||||
{{#each_row}}
|
||||
<div class="col-sm-6 col-lg-{{default size 3}} d-flex">
|
||||
<div class="card card-md flex-fill d-flex flex-column">
|
||||
{{#if icon}}
|
||||
<div class="ribbon ribbon-top ribbon-bookmark bg-{{default icon_color 'green'}}">
|
||||
{{~icon_img icon~}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div class="text-uppercase text-secondary font-weight-medium text-center">{{title}}</div>
|
||||
<div class="my-3 text-center"><span class="display-5 fw-bold text-{{value_color}}">{{value}}</span> {{#if small_text}}<small class="fs-6">{{small_text}}</small>{{/if}}</div>
|
||||
<ul class="list-unstyled lh-lg">
|
||||
<li class="mb-3">{{description}} {{#if description_md}}<small>{{{markdown description_md}}}</small>{{/if}}</li>
|
||||
{{#each item}}
|
||||
{{#if (eq (typeof this) 'string')}}
|
||||
<li><span class="text-indigo me-2">{{icon_img 'arrow-narrow-right'}}</span> {{this}}</li>
|
||||
{{/if}}
|
||||
{{#if (eq (typeof this) 'object')}}
|
||||
<li class="d-flex">
|
||||
<span class="{{#if color}}text-{{color}}{{/if}} me-{{#if icon}}2{{else}}5{{/if}}">{{~icon_img icon~}}</span>
|
||||
{{#if link}}
|
||||
<p><a href="{{link}}" class="text-decoration-none">{{description}}</a></p>
|
||||
{{else}}
|
||||
<p>{{description}}</p>
|
||||
{{/if}}
|
||||
{{#if description_md}}{{{markdown description_md}}}{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{#if button_text}}
|
||||
<div class="text-center">
|
||||
<a href="{{link}}" {{#if target}}target="{{target}}"{{/if}} class="btn w-100 {{#if button_color}}btn-{{button_color}}{{/if}}">{{button_text}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each_row}}
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="my-2 {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<a href="data:text/csv;charset=UTF-8,
|
||||
{{~#if bom~}}
|
||||
%EF%BB%BF{{! Byte order mark for Excel }}
|
||||
{{~/if~}}
|
||||
{{~#each_row~}}
|
||||
{{~#if (eq @row_index 0)~}}{{! header }}
|
||||
{{~#each this~}}
|
||||
{{~url_encode (csv_escape @key ../../separator)~}}
|
||||
{{~#if (not @last)~}}
|
||||
{{default ../../separator ","}}
|
||||
{{~/if~}}
|
||||
{{~/each~}}
|
||||
%0A{{! %0A = newline}}
|
||||
{{~/if~}}
|
||||
{{~#each this~}}
|
||||
{{~url_encode (csv_escape this ../../separator)~}}
|
||||
{{~#if (not @last)~}}
|
||||
{{default ../../separator ","}}
|
||||
{{~/if~}}
|
||||
{{~/each~}}
|
||||
%0A
|
||||
{{~/each_row~}}
|
||||
"
|
||||
download="{{default filename title}}.csv"
|
||||
class="btn btn-{{default color "primary"}}{{#if size}} btn-{{size}}{{/if}}">
|
||||
{{~icon_img (default icon "download")~}}
|
||||
{{default title "Download"}}
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
<div class="card my-2 {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
{{#if title}}
|
||||
<div class="card-header">
|
||||
{{#if image_url}}
|
||||
<img src="{{image_url}}" class="avatar avatar-lg me-2 rounded" width="32" height="32" alt="{{title}}" />
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
<span class="avatar avatar-sm me-2">{{icon_img icon}}</span>
|
||||
{{/if}}
|
||||
<h2 class="card-title">
|
||||
{{~title~}}
|
||||
{{#if description}}
|
||||
<small class="text-muted d-block">{{description}}</small>
|
||||
{{/if}}
|
||||
{{#if description_md}}
|
||||
<small class="text-muted d-block remove-bottom-margin">{{{markdown description_md}}}</small>
|
||||
{{/if}}
|
||||
</h2>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="card-body">
|
||||
<div class="datagrid">
|
||||
{{#each_row}}
|
||||
<div class="datagrid-item" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<div class="datagrid-title">{{title}}</div>
|
||||
<div class="datagrid-content {{#if active}}fw-bold{{/if}}">
|
||||
{{~#if link}}
|
||||
<a href="{{link}}"
|
||||
{{~else~}}
|
||||
<span
|
||||
{{/if~}}
|
||||
{{~#if tooltip}} data-bs-toggle="tooltip" data-bs-placement="top" title="{{tooltip}}"{{/if}}
|
||||
>
|
||||
{{#if color}}
|
||||
<span class="status status-{{color}} h-auto rounded-3 {{#if active}}fw-bold{{/if}}">
|
||||
{{/if}}
|
||||
{{#if image_url}}
|
||||
<img src="{{image_url}}" class="avatar avatar-xs me-1 rounded" width="32" height="32" alt="{{title}}" />
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
<span class="flex-shrink-0">{{~icon_img icon~}}</span>
|
||||
{{/if}}
|
||||
{{#if description}}
|
||||
{{description}}
|
||||
{{else}}
|
||||
–
|
||||
{{/if}}
|
||||
{{#if color}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{~#if link~}}
|
||||
</a>
|
||||
{{~else~}}
|
||||
</span>
|
||||
{{~/if~}}
|
||||
{{#if footer}}
|
||||
<small class="text-muted d-block">{{footer}}</small>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each_row}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<h2>Debug output</h2>
|
||||
<pre><code class="language-json">{{stringify this}}
|
||||
{{#each_row}}{{stringify this}}
|
||||
{{/each_row}}
|
||||
</code></pre>
|
||||
@@ -0,0 +1,17 @@
|
||||
{{#each_row}}
|
||||
{{#if tag}}
|
||||
<{{{tag}}}>
|
||||
{{~#each (entries this)~}}
|
||||
{{#unless (eq key "tag")}}
|
||||
{{~value~}}
|
||||
{{/unless}}
|
||||
{{~/each~}}
|
||||
</{{{tag}}}>
|
||||
{{else}}
|
||||
<p>
|
||||
{{~#each (entries this)}}
|
||||
{{value}}
|
||||
{{/each~}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/each_row}}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{#if contents}}
|
||||
<div class="
|
||||
hr-text
|
||||
{{~#if position}} hr-text-{{position}}{{/if}}
|
||||
{{~#if color}} text-{{color}} {{/if~}}
|
||||
{{~#if bold}} fw-bold {{/if~}}
|
||||
{{~#if italics}} fst-italic {{/if~}}
|
||||
{{~#if underline}} text-decoration-underline {{/if~}}
|
||||
{{~#if size}} fs-{{minus 7 size}} {{/if~}}
|
||||
{{~#if class}} {{class}}{{/if~}}
|
||||
">
|
||||
{{#if link}}
|
||||
<a
|
||||
href="{{link}}"
|
||||
class="{{#if color}} text-{{color}} {{/if}}"
|
||||
>
|
||||
{{contents}}
|
||||
</a>
|
||||
{{else}}
|
||||
{{contents}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
<hr class="{{class}}" />
|
||||
{{/if}}
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="empty{{#if class}} {{class}}{{/if}}"{{#if id}} id="{{id}}"{{/if}}>
|
||||
{{#if header}}
|
||||
<div class="empty-header">{{header}}</div>
|
||||
{{else}}
|
||||
{{#if icon}}
|
||||
<div class="empty-icon">
|
||||
<span>{{icon_img icon }}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if image}}
|
||||
<div class="empty-img"><img src="{{image}}" height="128" alt="{{image}}"/></div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<p class="empty-title">{{title}}</p>
|
||||
<div class="empty-subtitle text-secondary remove-bottom-margin">
|
||||
{{~#if description}}<p>{{description}}</p>{{/if~}}
|
||||
{{~#if description_md~}}
|
||||
{{{markdown description_md}}}
|
||||
{{~/if~}}
|
||||
</div>
|
||||
|
||||
<div class="empty-action">
|
||||
<a href="{{link}}" class="btn btn-primary">
|
||||
<span class="me-1">{{icon_img link_icon}}</span>
|
||||
{{link_text}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class="alert alert-danger mt-2" role="alert">
|
||||
<div class="alert-icon">
|
||||
{{icon_img 'alert-hexagon'}}
|
||||
</div>
|
||||
<div class="overflow-auto w-100">
|
||||
<h4 class="alert-title">
|
||||
{{#if query_number}}
|
||||
Error in query number <strong>{{query_number}}</strong>
|
||||
{{else}}
|
||||
An error occurred
|
||||
{{/if}}
|
||||
</h4>
|
||||
<div class="text-muted">
|
||||
<p>We are sorry, but an error occurred while generating this page. You should contact the site's
|
||||
administrator.</p>
|
||||
{{~#if description~}}
|
||||
<pre class="mt-2"><code class="sqlpage-error-description">{{description}}</code></pre>
|
||||
{{~/if~}}
|
||||
{{#if backtrace}}
|
||||
<details class="mt-2" open>
|
||||
<summary>Backtrace</summary>
|
||||
{{~#each backtrace~}}
|
||||
<pre class="fs-6 mt-1 p-1 my-1"><code>{{this}}</code></pre>
|
||||
{{~/each~}}
|
||||
</details>
|
||||
{{/if}}
|
||||
{{#if note}}
|
||||
<p class="fs-6 mt-1 p-1 my-1"><strong>Note:</strong> {{note}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="accordion mb-4 {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
{{#each_row}}
|
||||
<div class="accordion-item {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<h2 class="accordion-header" id="heading-{{@component_index}}-{{@row_index}}">
|
||||
<button class="accordion-button {{#if (not expanded)}}collapsed{{/if}}" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse-{{@component_index}}-{{@row_index}}" aria-controls="collapse-{{@component_index}}-{{@row_index}}">
|
||||
{{~#if title~}}
|
||||
{{title}}
|
||||
{{~else~}}
|
||||
{{@row_index}}
|
||||
{{/if}}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-{{@component_index}}-{{@row_index}}" class="accordion-collapse collapse {{#if expanded}}show{{/if}}"
|
||||
aria-labelledby="heading-{{@component_index}}-{{@row_index}}"
|
||||
data-bs-parent="#{{default id 'accordion_default'}}"
|
||||
>
|
||||
<div class="accordion-body">
|
||||
{{~#if description~}}
|
||||
<p>{{description}}</p>
|
||||
{{/if}}
|
||||
{{~#if description_md~}}
|
||||
{{{markdown description_md}}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each_row}}
|
||||
</div>
|
||||
@@ -0,0 +1,194 @@
|
||||
<form
|
||||
{{#if id}}id="{{id}}"{{/if}}
|
||||
class="mt-3 mb-1 {{class}}"
|
||||
method="{{default method "post"}}"
|
||||
{{#if enctype}}enctype="{{enctype}}"{{/if}}
|
||||
{{#if action}}action="{{action}}"
|
||||
{{else}}
|
||||
{{#if id}}action="#{{id}}"{{/if}}
|
||||
{{/if}}
|
||||
{{#if auto_submit}}data-auto-submit{{/if}}
|
||||
>
|
||||
<fieldset class="form-fieldset {{#if (or (and (ne validate '') (not auto_submit)) reset)}}mb-1{{else}}mb-0{{/if}}">
|
||||
{{#if title}}
|
||||
<h2 class="text-center mb-0">{{title}}</h2>
|
||||
{{/if}}
|
||||
<div class="row">
|
||||
{{#each_row}}
|
||||
{{#if (eq type "header")}}
|
||||
<h3 class="text-center mt-2 mb-1"{{#if id}} id="{{id}}"{{/if}}>{{label}}</h3>
|
||||
{{else}}
|
||||
{{#if (or (eq type "radio") (eq type "checkbox"))}}
|
||||
<div class="form-selectgroup form-selectgroup-boxes d-flex flex-column mx-0 my-1 col-md-{{default width 12}}">
|
||||
<label class="form-selectgroup-item flex-fill mx-0">
|
||||
<input type="{{type}}" {{#if id}}id="{{id}}" {{/if}}name="{{name}}" value="{{value}}" {{#if required}}required{{/if}} {{#if checked}}checked{{/if}} class="form-selectgroup-input">
|
||||
<div class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<div class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</div>
|
||||
<div>
|
||||
{{default label value}}
|
||||
{{~#if required}}
|
||||
<span class="text-danger ms-1" aria-label="required" title="required">*</span>
|
||||
{{/if}}
|
||||
{{#if description}}
|
||||
<small class="form-hint mt-0">{{description}}</small>
|
||||
{{/if}}
|
||||
{{#if description_md}}
|
||||
<small class="form-hint mt-0">{{{markdown description_md}}}</small>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{{else}}
|
||||
{{~#if (eq type "switch")}}
|
||||
<div class="form-selectgroup form-selectgroup-boxes d-flex flex-column mx-0 my-1 col-md-{{default width 12}}">
|
||||
<label class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" {{#if id}}id="{{id}}" {{/if}}name="{{name}}" value="{{value}}"{{#if required}} required{{/if}}{{#if checked}} checked{{/if}}{{#if disabled}} disabled{{/if~}}/>
|
||||
<span class="form-check-label">
|
||||
{{default label value}}
|
||||
{{~#if required}}
|
||||
<span class="text-danger ms-1" aria-label="required" title="required">*</span>
|
||||
{{/if}}
|
||||
{{#if description}}
|
||||
<small class="form-hint mt-0">{{description}}</small>
|
||||
{{/if}}
|
||||
{{#if description_md}}
|
||||
<small class="form-hint mt-0">{{{markdown description_md}}}</small>
|
||||
{{/if}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{{else}}
|
||||
{{~#if (eq type "hidden")}}
|
||||
<input type="hidden" name="{{name}}" {{#if id}}id="{{id}}" {{/if}}value="{{value}}">
|
||||
{{else}}
|
||||
<label class="form-label mb-2 col-md-{{default width 12}}">
|
||||
{{~default label name~}}
|
||||
{{~#if required}}
|
||||
<span class="text-danger ms-1" aria-label="required" title="required">*</span>
|
||||
{{/if}}
|
||||
{{~#if (eq type 'textarea')~}}
|
||||
<textarea
|
||||
name="{{name}}"
|
||||
class="form-control {{class}}"
|
||||
placeholder="{{placeholder}}"
|
||||
rows="{{default rows 3}}"
|
||||
{{#if id}}id="{{id}}" {{/if~}}
|
||||
{{~#if value includeZero=true}}value="{{value}}" {{/if~}}
|
||||
{{~#if minlength}}minlength="{{minlength}}" {{/if~}}
|
||||
{{~#if maxlength}}maxlength="{{maxlength}}" {{/if~}}
|
||||
{{~#if required}}required="required" {{/if~}}
|
||||
{{~#if autofocus}}autofocus {{/if~}}
|
||||
{{~#if disabled}}disabled {{/if~}}
|
||||
{{~#if readonly}}readonly {{/if~}}
|
||||
>
|
||||
{{~#if value includeZero=true}}{{value}}{{/if~}}
|
||||
</textarea>
|
||||
{{~else~}}{{#if (eq type 'select')~}}
|
||||
<select name="{{name}}"
|
||||
class="form-select {{class}}"
|
||||
{{~#if id}} id="{{id}}" {{/if~}}
|
||||
{{~#if required}} required="required" {{/if~}}
|
||||
{{~#if autofocus}} autofocus {{/if~}}
|
||||
{{~#if disabled}}disabled {{/if~}}
|
||||
{{~#if multiple}} multiple {{/if~}}
|
||||
{{~#if (or dropdown searchable options_source)}}
|
||||
data-pre-init="select-dropdown"
|
||||
data-sqlpage-js="{{static_path 'tomselect.js'}}"
|
||||
{{/if~}}
|
||||
{{~#if options_source}} data-options_source="{{options_source}}" {{/if~}}
|
||||
{{~#if placeholder}} placeholder="{{placeholder}}" {{/if~}}
|
||||
{{~#if create_new}} data-create_new={{create_new}} {{/if~}}
|
||||
>
|
||||
{{#if empty_option}}<option value="">{{empty_option}}</option>{{/if}}
|
||||
{{#each (parse_json options)}}
|
||||
<option value="{{value}}" {{#if (or (loose_eq ../value value) selected)}}selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<div class="input-group">
|
||||
{{#if prefix_icon}}<span class="input-group-text">{{icon_img prefix_icon}}</span>{{/if}}
|
||||
{{#if prefix}}<span class="input-group-text">{{prefix}}</span>{{/if}}
|
||||
<input name="{{name}}" class="form-control {{class}}"
|
||||
{{~#if id}} id="{{id}}" {{/if~}}
|
||||
{{~#if type}} type="{{type}}" {{/if~}}
|
||||
{{~#if placeholder includeZero=true}} placeholder="{{placeholder}}" {{/if~}}
|
||||
{{~#if value includeZero=true}} value="{{value}}" {{/if~}}
|
||||
{{~#if (or max (eq max 0))}} max="{{max}}" {{/if~}}
|
||||
{{~#if (or min (eq min 0))}} min="{{min}}" {{/if~}}
|
||||
{{~#if step}} step="{{step}}" {{/if~}}
|
||||
{{~#if minlength}} minlength="{{minlength}}" {{/if~}}
|
||||
{{~#if maxlength}} maxlength="{{maxlength}}" {{/if~}}
|
||||
{{~#if pattern}} pattern="{{pattern}}" {{/if~}}
|
||||
{{~#if required}} required="required" {{/if~}}
|
||||
{{~#if (or (eq autocomplete false) (eq autocomplete 0))}}autocomplete="off" {{/if~}}
|
||||
{{~#if (or (eq autocomplete true) (eq autocomplete 1))}}autocomplete="on" {{/if~}}
|
||||
{{~#if (gt (len autocomplete) 1)}}autocomplete="{{autocomplete}}" {{/if~}}
|
||||
{{~#if formaction}}formaction="{{formaction}}" {{/if~}}
|
||||
{{~#if formenctype}}formenctype="{{formenctype}}" {{/if~}}
|
||||
{{~#if formmethod}}formmethod="{{formmethod}}" {{/if~}}
|
||||
{{~#if formnovalidate}}formnovalidate="{{formnovalidate}}" {{/if~}}
|
||||
{{~#if formtarget}}formtarget="{{formtarget}}" {{/if~}}
|
||||
{{~#if list}}list="{{list}}" {{/if~}}
|
||||
{{~#if multiple}}multiple="{{multiple}}" {{/if~}}
|
||||
{{~#if accept}}accept="{{accept}}" {{/if~}}
|
||||
{{~#if autofocus}}autofocus {{/if~}}
|
||||
{{~#if disabled}}disabled {{/if~}}
|
||||
{{~#if readonly}}readonly {{/if~}}
|
||||
{{~#if (eq type "file")}}
|
||||
data-max-size="{{app_config "max_uploaded_file_size"}}"
|
||||
{{/if~}}
|
||||
/>
|
||||
{{#if suffix}}<span class="input-group-text">{{suffix}}</span>{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{~#if description~}}
|
||||
<small class="form-hint mt-0">{{description}}</small>
|
||||
{{~/if~}}
|
||||
{{~#if description_md~}}
|
||||
<small class="form-hint mt-0">{{{markdown description_md}}}</small>
|
||||
{{~/if~}}
|
||||
</label>
|
||||
{{~/if~}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (eq type "file")}}
|
||||
<!-- Change the form encoding type if this is a file input-->
|
||||
{{#delay}}formenctype="multipart/form-data"{{/delay}}
|
||||
{{/if}}
|
||||
{{/each_row}}
|
||||
</div>
|
||||
{{#if (and (ne validate '') (not auto_submit))}}
|
||||
<button class="btn
|
||||
btn-{{default validate_color "primary"}}
|
||||
{{#if validate_shape}} btn-{{validate_shape}} {{/if}}
|
||||
{{#if validate_outline}} btn-outline-{{validate_outline}} {{/if}}
|
||||
{{#if validate_size}} btn-{{validate_size}} {{/if}}"
|
||||
{{flush_delayed}}
|
||||
type="submit">
|
||||
{{~#if validate_icon~}}
|
||||
<span {{~#if (not narrow)}} class="me-1"{{/if}}>{{~icon_img validate_icon~}}</span>
|
||||
{{~/if~}}
|
||||
<span>{{#if validate}}{{validate}}{{else}}Submit Query{{/if}}</span>
|
||||
</button>
|
||||
|
||||
{{/if}}
|
||||
{{#if reset}}
|
||||
<button class="btn
|
||||
{{#if reset_color}} btn-{{reset_color}} {{/if}}
|
||||
{{#if reset_shape}} btn-{{reset_shape}} {{/if}}
|
||||
{{#if reset_outline}} btn-outline-{{reset_outline}} {{/if}}
|
||||
{{#if reset_size}} btn-{{reset_size}} {{/if}}"
|
||||
type="reset">
|
||||
{{~#if reset_icon~}}
|
||||
<span {{~#if (not narrow)}} class="me-1"{{/if}}>{{~icon_img reset_icon~}}</span>
|
||||
{{~/if~}}
|
||||
<span>{{reset}}</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
</form>
|
||||
@@ -0,0 +1,61 @@
|
||||
<header class="row align-items-center{{#if reverse}} flex-row-reverse{{/if}}" {{#if id}} id="{{id}}"{{/if}}>
|
||||
<div class="hero-title col text-center">
|
||||
<h1 class="lh-lg" style="font-size: 3rem">{{title}}</h1>
|
||||
<div class="fs-1 mx-5 text-muted">
|
||||
{{~description~}}
|
||||
{{~#if description_md~}}
|
||||
{{{markdown description_md}}}
|
||||
{{~/if~}}
|
||||
</div>
|
||||
{{#if link}}
|
||||
<a href="{{link}}" class="btn btn-primary mb-3 mt-2 text-wrap">{{default link_text "Go"}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if image}}
|
||||
<img src="{{image}}" alt="{{title}}" class="hero-image img-fluid col-lg-6" />
|
||||
{{/if}}
|
||||
{{#if video}}
|
||||
<video src="{{video}}" alt="{{title}}" class="hero-image img-fluid col-lg-6"
|
||||
{{~#if loop}} loop{{/if~}}
|
||||
{{~#if muted}} muted{{/if~}}
|
||||
{{~#if nocontrols}} autoplay
|
||||
{{~else}} controls
|
||||
{{~#if autoplay}} autoplay{{/if~}}
|
||||
{{~/if~}}
|
||||
{{~#if poster}} preload="none" poster="{{poster}}"{{/if~}}>
|
||||
</video>
|
||||
{{/if}}
|
||||
</header>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="row justify-content-center mt-2">
|
||||
{{~#each_row~}}
|
||||
<div class="col-lg-6 col-xxl-4 mb-4">
|
||||
<div class="card border-0 h-100">
|
||||
<div class="card-body text-center p-4 p-lg-5 pt-0 pt-lg-0">
|
||||
{{#if icon}}
|
||||
<div style="margin-top: -1.5rem;" class="badge bg-{{default color 'success'}} text-{{default color 'success'}}-fg fs-1 mb-4 p-2">
|
||||
{{~icon_img icon 30~}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<h2>
|
||||
{{#if link}}
|
||||
<a href="{{link}}">
|
||||
{{/if}}
|
||||
{{title}}
|
||||
{{#if link}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</h2>
|
||||
<div class="mb-0">
|
||||
{{~description~}}
|
||||
{{~#if description_md~}}
|
||||
{{{markdown description_md}}}
|
||||
{{~/if~}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{~/each_row~}}
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
{{{~html~}}}
|
||||
{{~#each_row~}}
|
||||
{{{~html~}}}
|
||||
{{~text~}}
|
||||
{{{~post_html~}}}
|
||||
{{~/each_row~}}
|
||||
@@ -0,0 +1,83 @@
|
||||
<div class="card my-2 {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
{{#if title}}
|
||||
<div class="card-header {{#if compact}}py-2{{/if}}">
|
||||
<h2 class="card-title">{{title}}</h2>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="list-group list-group-flush list-group-hoverable">
|
||||
{{#each_row}}
|
||||
<div {{#if id}}id="{{id}}"{{/if}}
|
||||
class="list-group-item list-group-item-action p-0 {{#if active}}active{{/if}} {{class}}">
|
||||
<div class="row align-items-center m-0">
|
||||
{{#if link}}<a href="{{link}}"
|
||||
{{~else}}<div {{/if}}
|
||||
class="col row align-items-center text-decoration-none text-body m-0 {{#if ../compact}}p-1{{else}}p-3{{/if}}">
|
||||
{{#if color}}
|
||||
<div class="col-auto"><span class="badge bg-{{color}}"></span></div>
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
<div class="col-auto fs-2">
|
||||
{{~icon_img icon~}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if image_url}}
|
||||
<div class="col-auto">
|
||||
<img src="{{image_url}}" class="avatar avatar-md rounded" width="45" height="45" alt="{{title}}" />
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="col {{#if ../wrap}}text-wrap{{else}}text-truncate{{/if}}">
|
||||
{{title}}
|
||||
{{~#if (or description description_md)~}}
|
||||
<div class="text-muted text-truncate{{#if ../compact}} mt-n1{{/if}}{{#if ../wrap}} text-wrap{{/if}}">
|
||||
{{~description~}}
|
||||
{{~#if description_md~}}
|
||||
<div style="margin-bottom: -1rem">{{{markdown description_md}}}</div>
|
||||
{{~/if~}}
|
||||
</div>
|
||||
{{~/if~}}
|
||||
</div>
|
||||
|
||||
{{#if link}}
|
||||
</a>
|
||||
{{else}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if view_link}}
|
||||
<a href="{{view_link}}" class="link-secondary col-auto" title="View">
|
||||
{{~icon_img 'eye'~}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if delete_link}}
|
||||
<form method="POST" action="{{delete_link}}" class="col-auto m-0 p-0">
|
||||
<button type="submit" class="btn btn-link link-secondary p-0 border-0 bg-transparent" title="Delete">
|
||||
{{~icon_img 'trash'~}}
|
||||
</button>
|
||||
</form>
|
||||
{{/if}}
|
||||
|
||||
{{#if edit_link}}
|
||||
<a href="{{edit_link}}" class="link-secondary col-auto" title="Edit">
|
||||
{{~icon_img 'edit'~}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each_row}}
|
||||
{{#if (eq @row_index 0)}}
|
||||
<a href="{{default empty_link '#'}}" class="list-group-item list-group-item-action">
|
||||
<div class="row align-items-center">
|
||||
<div class="col {{#if ../wrap}}text-wrap{{else}}text-truncate{{/if}}">
|
||||
{{default empty_title 'No item'}}
|
||||
<div class="text-muted text-truncate{{#if ../compact}} mt-n1{{/if}}{{#if ../wrap}} text-wrap{{/if}}">
|
||||
{{~empty_description~}}
|
||||
{{~#if empty_description_md~}}
|
||||
<div style="margin-bottom: -1rem">{{{markdown empty_description_md}}}</div>
|
||||
{{~/if~}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,82 @@
|
||||
<div class="container" {{class}}>
|
||||
<div class="row d-flex justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<form
|
||||
{{#if id}}id="{{id}}"{{/if}}
|
||||
class="mt-3 mb-1"
|
||||
method="post"
|
||||
{{#if enctype}}enctype="{{enctype}}"{{/if}}
|
||||
{{#if action}}
|
||||
action="{{action}}"
|
||||
{{else}}
|
||||
{{#if id}}action="#{{id}}"{{/if}}
|
||||
{{/if}}
|
||||
>
|
||||
{{#if image}}
|
||||
<div class="form-group d-flex justify-content-center align-items-center mb-2">
|
||||
<img src="{{image}}"/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if title}}
|
||||
<h1 class="text-center mb-3">{{title}}</h1>
|
||||
{{/if}}
|
||||
{{#if (or error_message error_message_md)}}
|
||||
<div class="alert alert-danger mb-3" role="alert">
|
||||
<div class="alert-icon">
|
||||
{{icon_img 'alert-circle'}}
|
||||
</div>
|
||||
<div class="overflow-auto w-100 remove-bottom-margin">
|
||||
{{~error_message~}}
|
||||
{{{~markdown error_message_md~}}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<label class="form-label" for="username">{{username}}</label>
|
||||
<div class="input-icon mb-3">
|
||||
<span class="input-icon-addon">{{icon_img (default username_icon 'user-circle')}}</span>
|
||||
<input type="text" name="username" id="username" value="" class="form-control" placeholder="{{username}}" required="required" autofocus autocomplete="username"/>
|
||||
</div>
|
||||
<label class="form-label" for="password">{{password}}
|
||||
{{#if forgot_password_text}}
|
||||
<span class="form-label-description"><a href="{{forgot_password_link}}">{{forgot_password_text}}</a></span>
|
||||
{{/if}}
|
||||
</label>
|
||||
<div class="input-icon mb-3">
|
||||
<span class="input-icon-addon">{{~icon_img (default password_icon 'key')~}}</span>
|
||||
<input type="password" name="password" id="password" value="" class="form-control" placeholder="{{password}}" required="required" autocomplete="current-password" />
|
||||
</div>
|
||||
{{#if remember_me_text}}
|
||||
<label class="form-check">
|
||||
<input class="form-check-input" id="remember" name="remember" type="checkbox" />
|
||||
<span class="form-check-label">{{remember_me_text}}</span>
|
||||
</label>
|
||||
{{/if}}
|
||||
<div class="form-group d-flex justify-content-center align-items-center">
|
||||
<input
|
||||
class="btn
|
||||
btn-{{default validate_color "primary"}}
|
||||
{{#if validate_shape}} btn-{{validate_shape}} {{/if}}
|
||||
{{#if validate_outline}} btn-outline-{{validate_outline}} {{/if}}
|
||||
{{#if validate_size}} btn-{{validate_size}} {{/if}}"
|
||||
type="submit"
|
||||
name="submit"
|
||||
value="{{default validate 'Login'}}"/>
|
||||
</div>
|
||||
{{#if (or footer footer_md)}}
|
||||
<hr>
|
||||
<div class="text-center mb-0">
|
||||
<small class="form-hint mt-0">
|
||||
{{#if footer}}
|
||||
{{footer}}
|
||||
{{else}}
|
||||
{{#if footer_md}}
|
||||
{{{markdown footer_md}}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</small>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="card my-3 {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{title}}</h3>
|
||||
<div
|
||||
class="leaflet"
|
||||
style="height: {{default height 350}}px;"
|
||||
{{~#if latitude includeZero=true}} data-center="{{latitude}},{{longitude}}"{{/if}}
|
||||
data-zoom="{{default zoom 5}}"
|
||||
data-attribution="{{default attribution '© OpenStreetMap'}}"
|
||||
{{~#if (ne tile_source false)}} data-tile_source="{{default tile_source 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}}"{{/if}}
|
||||
data-max_zoom="{{default max_zoom 18}}"
|
||||
data-pre-init="map"
|
||||
>
|
||||
<div class="d-flex justify-content-center h-100 align-items-center">
|
||||
<div
|
||||
class="spinner-border"
|
||||
role="status"
|
||||
style="width: 3rem; height: 3rem;"
|
||||
>
|
||||
<span class="visually-hidden">Loading map...</span>
|
||||
</div>
|
||||
<div class="d-none" hidden>
|
||||
{{~#each_row~}}
|
||||
<div class="marker"
|
||||
{{~#if latitude includeZero=true}} data-coords="{{latitude}},{{longitude}}"{{/if}}
|
||||
{{~#if color}} data-color="{{color}}"{{/if}}
|
||||
{{~#if size}} data-size="{{size}}"{{/if}}
|
||||
{{~#if link}} data-link="{{link}}"{{/if}}
|
||||
{{~#if geojson}} data-geojson="
|
||||
{{~#if (eq (typeof geojson) 'string')}}{{geojson}}
|
||||
{{~else~}}{{stringify geojson}}
|
||||
{{~/if~}}
|
||||
"{{/if~}}
|
||||
>
|
||||
<h3>
|
||||
{{~#if icon~}}
|
||||
<span class="mapicon">{{~icon_img icon size~}}</span>
|
||||
{{~/if~}}
|
||||
{{~#if link~}}
|
||||
<a href="{{link}}">{{title}}</a>
|
||||
{{~else~}}
|
||||
{{title}}
|
||||
{{~/if~}}
|
||||
</h3>
|
||||
<div class="description">
|
||||
{{~#if description}}<p>{{description}}</p>{{/if~}}
|
||||
{{~#if description_md~}}
|
||||
{{{markdown description_md}}}
|
||||
{{~/if~}}
|
||||
</div>
|
||||
</div>
|
||||
{{~/each_row~}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="modal {{~#if class}} {{class}}{{/if~}}"
|
||||
id="{{id}}"
|
||||
tabindex="-1"
|
||||
aria-hidden="false"
|
||||
aria-labelledby="{{title}}">
|
||||
<div role="document" class="modal-dialog {{#if small}} modal-sm{{/if}}{{#if large}} modal-lg{{/if}}{{#if scrollable}} modal-dialog-scrollable{{/if}}">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{title}}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{default close "close"}}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card bg-transparent border-0"
|
||||
{{~#if (and embed (ne embed_mode "iframe"))}} data-pre-init="card" data-embed="{{embed}}"{{/if ~}}
|
||||
>
|
||||
<div class="card-body p-0">
|
||||
<div class="card-content remove-bottom-margin"></div>
|
||||
{{~#if embed ~}}
|
||||
{{~#if (eq embed_mode "iframe")}}
|
||||
<iframe src="{{embed}}"
|
||||
width="100%"
|
||||
{{~#if height}} height="{{height}}"{{/if~}}
|
||||
{{~#if allow}} allow="{{allow}}"{{/if~}}
|
||||
{{~#if sandbox}} sandbox="{{sandbox}}"{{/if~}}
|
||||
{{~#if style}} style="{{style}}"{{/if~}}
|
||||
>
|
||||
</iframe>
|
||||
{{~else~}}
|
||||
<div class="d-flex justify-content-center h-100 align-items-center card-loading-placeholder">
|
||||
<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
{{~/if~}}
|
||||
{{~/if~}}
|
||||
{{~#each_row~}}
|
||||
<p>
|
||||
{{~#if contents_md~}}
|
||||
{{{markdown contents_md}}}
|
||||
{{else}}
|
||||
{{~contents~}}
|
||||
{{~/if~}}
|
||||
</p>
|
||||
{{~/each_row~}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{#if close}}<button type="button" class="btn me-primary" data-bs-dismiss="modal">{{close}}</button>{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
<div class="container {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<div class="p-3 d-flex align-items-center flex-column">
|
||||
<ul class="pagination{{#if outline}} pagination-outline{{/if}}{{#if circle}} pagination-circle{{/if}}">
|
||||
{{#if first_link}}
|
||||
<li class="page-item{{#if first_disabled}} disabled{{/if}}">
|
||||
{{#if first_title}}
|
||||
<a class="page-link page-text" href="{{first_link}}" tabindex="-1" aria-disabled="true">{{first_title}}</a>
|
||||
{{else}}
|
||||
<a class="page-link" href="{{first_link}}" tabindex="-1" aria-disabled="true">{{icon_img 'chevrons-left' 19}}</a>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if previous_link}}
|
||||
<li class="page-item{{#if previous_disabled}} disabled{{/if}}">
|
||||
{{#if previous_title}}
|
||||
<a class="page-link page-text" href="{{previous_link}}" tabindex="-1" aria-disabled="true">{{previous_title}}</a>
|
||||
{{else}}
|
||||
<a class="page-link " href="{{previous_link}}" tabindex="-1" aria-disabled="true">{{icon_img 'chevron-left' 19}}</a>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{~#each_row~}}
|
||||
<li class="page-item{{#if active}} active{{/if}}">
|
||||
{{#if offset}}
|
||||
<span class="page-link disabled">…</span>
|
||||
{{else}}
|
||||
<a class="page-link" href="{{default link '#'}}">{{contents}}</a>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{~/each_row~}}
|
||||
{{#if next_link}}
|
||||
<li class="page-item{{#if next_disabled}} disabled{{/if}}">
|
||||
{{#if next_title}}
|
||||
<a class="page-link page-text" href="{{next_link}}" tabindex="-1" aria-disabled="true">{{next_title}}</a>
|
||||
{{else}}
|
||||
<a class="page-link" href="{{next_link}}" tabindex="-1" aria-disabled="true">{{icon_img 'chevron-right' 19}}</a>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if last_link}}
|
||||
<li class="page-item{{#if last_disabled}} disabled{{/if}}">
|
||||
{{#if last_title}}
|
||||
<a class="page-link page-text" href="{{last_link}}" tabindex="-1" aria-disabled="true">{{last_title}}</a>
|
||||
{{else}}
|
||||
<a class="page-link" href="{{last_link}}" tabindex="-1" aria-disabled="true">{{icon_img 'chevrons-right' 19}}</a>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0"
|
||||
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
|
||||
xmlns:podcast="https://podcastindex.org/namespace/1.0"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>{{title}}</title>
|
||||
<link>{{link}}</link>
|
||||
<description>{{description}}</description>
|
||||
{{~#if language}}<language>{{language}}</language>{{/if}}
|
||||
{{~#if category}}<itunes:category text="{{category}}">{{sub_category}}</itunes:category>{{/if}}
|
||||
<itunes:explicit>{{#if explicit}}true{{else}}false{{/if}}</itunes:explicit>
|
||||
{{~#if image_url}}<itunes:image href="{{image_url}}" />{{/if}}
|
||||
{{~#if author}}<itunes:author>{{author}}</itunes:author>{{/if}}
|
||||
{{~#if copyright}}<copyright>{{copyright}}</copyright>{{/if}}
|
||||
{{~#if funding_url}}<podcast:funding url="{{funding_url}}">{{funding_text}}</podcast:funding>{{/if}}
|
||||
{{~#if type}}<itunes:type>{{type}}</itunes:type>{{/if}}
|
||||
{{~#if complete}}<itunes:complete>yes</itunes:complete>{{/if}}
|
||||
{{~#if locked}}<podcast:locked>yes</podcast:locked>{{/if}}
|
||||
{{~#if guid}}<podcast:guid>{{guid}}</podcast:guid>{{/if}}
|
||||
{{~#if self_link}}<atom:link href="{{self_link}}" rel="self" type="application/rss+xml" />{{/if}}
|
||||
{{#each_row}}
|
||||
<item>
|
||||
<title>{{title}}</title>
|
||||
<link>{{link}}</link>
|
||||
<description>{{description}}</description>
|
||||
{{~#if date}}<pubDate>{{rfc2822_date date}}</pubDate>{{/if}}
|
||||
{{~#if enclosure_url}}<enclosure length="{{enclosure_length}}" type="{{enclosure_type}}" url="{{enclosure_url}}" />{{/if}}
|
||||
{{~#if guid}}<guid isPermaLink="false">{{guid}}</guid>{{/if}}
|
||||
{{~#if episode}}<itunes:episode>{{episode}}</itunes:episode>{{/if}}
|
||||
{{~#if season}}<itunes:season>{{season}}</itunes:season>{{/if}}
|
||||
{{~#if episode_type}}<itunes:episodeType>{{episode_type}}</itunes:episodeType>{{/if}}
|
||||
{{~#if block}}<itunes:block>yes</itunes:block>{{/if}}
|
||||
{{~#if (not (eq explicit NULL))}}<itunes:explicit>{{#if explicit}}true{{else}}false{{/if}}</itunes:explicit>{{/if}}
|
||||
{{~#if image_url}}<itunes:image href="{{image_url}}" />{{/if}}
|
||||
{{~#if duration}}<itunes:duration>{{duration}}</itunes:duration>{{/if}}
|
||||
{{~#if transcript_url}}<podcast:transcript url="{{transcript_url}}" type="{{transcript_type}}" />{{/if}}
|
||||
</item>
|
||||
{{/each_row}}
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,3 @@
|
||||
{{{~html~}}}
|
||||
{{{~contents~}}}
|
||||
{{~#each_row~}}{{~/each_row~}}
|
||||
@@ -0,0 +1,241 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
lang="{{language}}"
|
||||
style="font-size: {{default font_size 18}}px"
|
||||
{{#if class}}class="{{class}}" {{/if}}
|
||||
{{~#if rtl}}dir="rtl" {{/if~}}
|
||||
>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{default title "SQLPage"}}</title>
|
||||
<link rel="icon" href="{{#if favicon}}{{favicon}}{{else}}{{static_path 'favicon.svg'}}{{/if}}">
|
||||
{{#if manifest}}
|
||||
<link rel="manifest" href="{{manifest}}">
|
||||
{{/if}}
|
||||
<link rel="stylesheet" href="{{static_path 'sqlpage.css'}}">
|
||||
{{#each (to_array css)}}
|
||||
{{#if this}}
|
||||
<link rel="stylesheet" href="{{this}}">
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if font}}
|
||||
{{#if (starts_with font "/")}}
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'LocalFont';
|
||||
src: url('{{font}}') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'LocalFont', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
{{else}}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family={{font}}&display=fallback">
|
||||
<style>
|
||||
:root {
|
||||
--tblr-font-sans-serif: '{{font}}',
|
||||
Arial,
|
||||
sans-serif;
|
||||
}
|
||||
</style>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<script src="{{static_path 'sqlpage.js'}}" defer nonce="{{@csp_nonce}}"></script>
|
||||
{{#each (to_array javascript)}}
|
||||
{{#if this}}
|
||||
<script src="{{this}}" defer nonce="{{@../csp_nonce}}"></script>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{#each (to_array javascript_module)}}
|
||||
{{#if this}}
|
||||
<script src="{{this}}" type="module" defer nonce="{{@../csp_nonce}}"></script>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{{#if title}}
|
||||
<meta property="og:title" content="{{title}}" />
|
||||
{{/if}}
|
||||
{{#if description}}
|
||||
<meta name="description" content="{{description}}" />
|
||||
<meta property="og:description" content="{{description}}" />
|
||||
{{/if}}
|
||||
{{#if preview_image}}
|
||||
<meta property="og:image" content="{{preview_image}}" />
|
||||
<meta name="twitter:image" content="{{preview_image}}" />
|
||||
{{/if}}
|
||||
|
||||
{{#if norobot}}
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
{{/if}}
|
||||
|
||||
{{#if refresh}}
|
||||
<meta http-equiv="refresh" content="{{refresh}}">
|
||||
{{/if}}
|
||||
{{#if rss}}
|
||||
<link rel="alternate" type="application/rss+xml" title="{{title}}" href="{{rss}}">
|
||||
{{/if}}
|
||||
<meta name="generator" content="SQLPage v{{buildinfo 'CARGO_PKG_VERSION'}}" />
|
||||
{{#if social_image}}
|
||||
<meta property="og:image" content="{{social_image}}" />
|
||||
{{/if}}
|
||||
</head>
|
||||
|
||||
{{!-- Partial for menu_items to not duplicate logic --}}
|
||||
{{#*inline "menu-items"}}
|
||||
<ul class="navbar-nav {{#if sidebar}}pt-lg-3{{else}}ms-auto{{/if}}">
|
||||
{{~#each (to_array menu_item)~}}
|
||||
{{~#if (or (eq (typeof this) 'object') (and (eq (typeof this) 'string') (starts_with this '{')))}}
|
||||
{{~#with (parse_json this)}}
|
||||
{{#if (or (or this.title this.icon) this.image)}}
|
||||
<li class="nav-item{{#if this.submenu}} dropdown{{/if}}{{#if this.active}} active{{/if}}">
|
||||
<a class="nav-link {{#if this.active}}active {{/if}}{{#if this.submenu}}dropdown-toggle{{/if}}" href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
|
||||
{{~#if this.submenu}}
|
||||
data-bs-toggle="dropdown"
|
||||
{{!-- commented out until this is fixed: https://github.com/tabler/tabler/issues/2485
|
||||
data-bs-auto-close="outside"
|
||||
--}}
|
||||
{{/if~}}
|
||||
{{#if this.target}}target="{{this.target}}"{{/if}}
|
||||
role="button"
|
||||
>
|
||||
{{~#if this.image~}}
|
||||
<span {{~#if this.title}} class="me-1"{{/if}}>
|
||||
{{~#if (eq this.size 'sm')}}
|
||||
<img width=16 height=16 src="{{this.image}}">
|
||||
{{~else~}}
|
||||
<img width=24 height=24 src="{{this.image}}">
|
||||
{{~/if~}}
|
||||
</span>
|
||||
{{~/if~}}
|
||||
{{#if this.icon}}
|
||||
{{#if this.title}}<span class="me-1">{{/if}}
|
||||
{{~icon_img this.icon~}}
|
||||
{{#if this.title}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{~this.title~}}
|
||||
</a>
|
||||
{{~#if this.submenu~}}
|
||||
<div class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
|
||||
{{~#each this.submenu~}}
|
||||
{{#if (or (or this.title this.icon) this.image)}}
|
||||
<a class="dropdown-item{{#if this.active}} active{{/if}}" href="{{this.link}}" {{#if this.target}}target="{{this.target}}"{{/if}}>
|
||||
{{~#if this.image~}}
|
||||
<span {{~#if this.title}} class="me-1"{{/if}}>
|
||||
{{~#if (eq ../this.size 'sm')}}
|
||||
<img width=16 height=16 src="{{this.image}}">
|
||||
{{~else~}}
|
||||
<img width=24 height=24 src="{{this.image}}">
|
||||
{{~/if~}}
|
||||
</span>
|
||||
{{~/if~}}
|
||||
{{#if this.icon}}
|
||||
{{#if this.title}}<span class="me-1">{{/if}}
|
||||
{{~icon_img this.icon~}}
|
||||
{{#if this.title}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{~this.title~}}
|
||||
</a>
|
||||
{{~/if~}}
|
||||
{{~/each~}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{~else}}
|
||||
{{~#if (gt (len this) 0)~}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-capitalize" href="{{this}}.sql">{{this}}</a>
|
||||
</li>
|
||||
{{~/if~}}
|
||||
{{~/if~}}
|
||||
{{~/each}}
|
||||
</ul>
|
||||
{{#if search_target}}
|
||||
<form class="d-flex" role="search" action="{{search_target}}">
|
||||
<input class="form-control me-2" type="search" placeholder="{{default search_placeholder 'Search'}}" aria-label="Search" name="search" value="{{search_value}}">
|
||||
<button class="btn btn-outline-success" type="submit">{{default search_button 'Search'}}</button>
|
||||
</form>
|
||||
{{/if}}
|
||||
{{/inline}}
|
||||
|
||||
<body class="layout-{{#if sidebar}}fluid{{else}}{{default layout 'boxed'}}{{/if}}" {{#if theme}}data-bs-theme="{{theme}}" {{/if}}>
|
||||
<div class="page">
|
||||
{{#if (or (or title (or icon image)) (or menu_item search_target))}}
|
||||
<header id="sqlpage_header">
|
||||
{{#if sidebar}}
|
||||
<aside class="navbar navbar-vertical navbar-expand-lg" {{#if sidebar_theme}}data-bs-theme="{{sidebar_theme}}" {{/if}}>
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler collapsed" type="button" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" data-bs-toggle="collapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<span class="navbar-brand navbar-brand-autodark d-inline ps-2 text-truncate">
|
||||
<a class="text-decoration-none text-body" href="{{#if link}}{{link}}{{else}}/{{/if}}">
|
||||
{{#if image}}
|
||||
<img src="{{image}}" alt="{{title}}" height="32" class="navbar-brand-image">
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
{{~icon_img icon~}}
|
||||
{{/if}}
|
||||
<span class="pe-2 pe-lg-0 align-middle">{{title}}</span>
|
||||
</a>
|
||||
</span>
|
||||
<div class="navbar-collapse collapse" id="sidebar-menu">
|
||||
{{> menu-items menu_item=menu_item}}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
{{else}}
|
||||
<nav class="navbar navbar-expand-md navbar-light{{#if fixed_top_menu}} fixed-top{{/if}}">
|
||||
<div class="container-fluid gap-2 justify-content-start" style="min-width:0">
|
||||
<a class="navbar-brand" href="{{#if link}}{{link}}{{else}}/{{/if}}">
|
||||
{{#if image}}
|
||||
<img src="{{image}}" alt="{{title}}" width="32" height="32" class="navbar-brand-image">
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
{{~icon_img icon~}}
|
||||
{{/if}}
|
||||
</a>
|
||||
<span class="mb-0 fs-2 text-truncate flex-grow-1" style="flex-basis:0">
|
||||
<a class="text-decoration-none text-body" href="{{#if link}}{{link}}{{else}}/{{/if}}">{{default navbar_title title}}</a>
|
||||
</span>
|
||||
{{#if (or menu_item search_target)}}
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse flex-grow-0" id="navbar-menu">
|
||||
{{> menu-items menu_item=menu_item}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="page-wrapper">
|
||||
<main class="page-body container-xl flex-grow-1 px-md-5 px-sm-3 {{#if fixed_top_menu}}mt-5{{#unless (eq layout 'boxed')}} pt-5{{/unless}}{{else}} mt-3{{/if}}" id="sqlpage_main_wrapper">
|
||||
{{~#each_row~}}{{~/each_row~}}
|
||||
</main>
|
||||
|
||||
{{#unless (eq footer '')}}
|
||||
<footer class="w-100 text-center fs-6 my-2 text-secondary" id="sqlpage_footer">
|
||||
{{#if footer}}
|
||||
{{{markdown footer}}}
|
||||
{{else}}
|
||||
<!-- You can change this footer using the 'footer' parameter of the 'shell' component -->
|
||||
Built with <a class="text-reset" href="https://sql-page.com"
|
||||
title="SQLPage v{{buildinfo 'CARGO_PKG_VERSION'}}">SQLPage</a>
|
||||
{{/if}}
|
||||
</footer>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
{{#if id}}<a id="{{id}}" />{{/if}}
|
||||
{{#if title}}
|
||||
<h2 class="mt-3 mb-0">{{title}}</h2>
|
||||
{{/if}}
|
||||
{{#if description}}
|
||||
<p>{{description}}</p>
|
||||
{{/if}}
|
||||
<div class="steps {{#if color}}steps-{{color}}{{/if}} {{#if counter}}steps-counter{{/if}} my-2">
|
||||
{{#each_row}}
|
||||
{{#if link}}
|
||||
<a href="{{link}}"
|
||||
{{else}}
|
||||
<span
|
||||
{{/if}}
|
||||
class="step-item px-1 {{#if active}}active{{/if}}"
|
||||
{{#if description}}data-bs-toggle="tooltip" title="{{description}}"{{/if}}
|
||||
>
|
||||
{{#if icon}}
|
||||
{{~icon_img icon~}}
|
||||
{{/if}}
|
||||
{{title}}
|
||||
{{#if link}}</a>{{else}}</span>{{/if}}
|
||||
{{/each_row}}
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<ul class="nav nav-bordered mb-3 {{#if center}}justify-content-evenly{{/if}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
{{~#each_row~}}
|
||||
<li class="nav-item {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<a
|
||||
class="nav-link {{#if active~}}
|
||||
active
|
||||
{{~/if}} {{#if color~}}
|
||||
text-{{color}} border-{{color}}
|
||||
{{~/if}} {{#if center~}}
|
||||
mx-auto
|
||||
{{~/if}}"
|
||||
|
||||
href="{{#if link}}
|
||||
{{~link~}}
|
||||
{{~else~}}
|
||||
?tab={{title}}{{#if id}}#{{id}}{{/if}}
|
||||
{{~/if}}"
|
||||
|
||||
{{#if description~}}
|
||||
title="{{description}}"
|
||||
{{~/if}}
|
||||
>
|
||||
{{~#if icon~}}
|
||||
<span class="me-1">
|
||||
{{~icon_img icon 20~}}
|
||||
</span>
|
||||
{{~/if}}
|
||||
{{~title~}}
|
||||
</a>
|
||||
</li>
|
||||
{{~/each_row~}}
|
||||
</ul>
|
||||
@@ -0,0 +1,140 @@
|
||||
<div class="card my-2 {{class}}" {{#if overflow}}style="width: fit-content;"{{/if}} {{#if id}}id="{{id}}"{{/if}}>
|
||||
<div class="card-body p-0" data-pre-init="table">
|
||||
{{#if (or search initial_search_value)}}
|
||||
<div class="p-3">
|
||||
<input
|
||||
id="{{component_index}}-search"
|
||||
type="search"
|
||||
class="form-control form-control-rounded fs-6 search"
|
||||
placeholder="{{default search_placeholder 'Search…'}}"
|
||||
value="{{initial_search_value}}"
|
||||
{{#if initial_search_value}}autocomplete="off"{{/if}}
|
||||
>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="table-responsive
|
||||
{{~#if freeze_columns}} table-freeze-columns text-nowrap {{/if~}}
|
||||
{{~#if freeze_headers}} table-freeze-headers text-nowrap {{/if~}}
|
||||
{{~#if freeze_footers}} table-freeze-footers text-nowrap {{/if~}}
|
||||
">
|
||||
<table class="table
|
||||
{{~#if striped_rows}} table-striped {{/if~}}
|
||||
{{~#if striped_columns}} table-striped-columns {{/if~}}
|
||||
{{~#if hover}} table-hover {{/if~}}
|
||||
{{~#if border}} table-bordered {{/if~}}
|
||||
{{~#if small}} table-sm {{/if~}}
|
||||
"
|
||||
{{~#if number_format_locale}} data-number_format_locale="{{number_format_locale}}"{{/if~}}
|
||||
{{~#if number_format_digits}} data-number_format_digits="{{number_format_digits}}"{{/if~}}
|
||||
{{~#if currency}} data-currency="{{currency}}"{{/if~}}
|
||||
>
|
||||
{{#if description}}<caption class="text-center text-muted">{{description}}</caption>{{/if}}
|
||||
{{#each_row}}
|
||||
{{#if (eq @row_index 0)}}
|
||||
{{! Since we are inside the first data row, render the header }}
|
||||
<thead>
|
||||
<tr>
|
||||
{{#each this}}
|
||||
{{#if (not (starts_with @key '_sqlpage_'))}}
|
||||
<th class="
|
||||
_col_{{replace @key ' ' '_'~}}
|
||||
{{~#if (array_contains_case_insensitive ../../align_right @key)}} text-end {{/if~}}
|
||||
{{~#if (array_contains_case_insensitive ../../align_center @key)}} text-center {{/if~}}
|
||||
"
|
||||
data-column_type="{{typeof this}}"
|
||||
{{~#if (array_contains_case_insensitive ../../raw_numbers @key)}} data-raw_number="1"{{/if~}}
|
||||
{{~#if (array_contains_case_insensitive ../../money @key)}} data-money="1"{{/if~}}
|
||||
>
|
||||
{{~#if ../../sort~}}
|
||||
<button class="table-sort sort d-inline" data-sort="{{@key}}">{{@key}}</button>
|
||||
{{~else~}}
|
||||
{{~@key~}}
|
||||
{{~/if~}}
|
||||
</th>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{#if ../edit_url}}<th class="_col_edit text-center">Edit</th>{{/if}}
|
||||
{{#if ../delete_url}}<th class="_col_delete text-center">Delete</th>{{/if}}
|
||||
{{#each (to_array ../custom_actions)}}
|
||||
<th class="_col_custom text-center">{{this.name}}</th>
|
||||
{{/each}}
|
||||
|
||||
{{#each (to_array _sqlpage_actions)}}
|
||||
<th class="_col_action text-center">{{this.name}}</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody list">{{#delay}}</tbody>{{/delay}}
|
||||
{{~/if~}}
|
||||
{{!~ If this data row should go into the footer, close the <tbody>, open the <tfoot> ~}}
|
||||
{{~#if _sqlpage_footer~}} </tbody><tfoot> {{/if~}}
|
||||
<tr class="{{_sqlpage_css_class}} {{#if _sqlpage_color}}bg-{{_sqlpage_color}}-lt{{/if}}" {{#if _sqlpage_id}}id="{{_sqlpage_id}}"{{/if}}>
|
||||
{{~#each this~}}
|
||||
{{~#if (not (starts_with @key '_sqlpage_'))~}}
|
||||
<td class="align-middle _col_{{replace @key ' ' '_'~}}
|
||||
{{~#if (array_contains_case_insensitive ../../align_right @key)}} text-end {{/if~}}
|
||||
{{~#if (array_contains_case_insensitive ../../align_center @key)}} text-center {{/if~}}
|
||||
{{~#if (array_contains_case_insensitive ../../monospace @key)}} font-monospace {{/if~}}
|
||||
">
|
||||
{{~#if (array_contains_case_insensitive ../../markdown @key)~}}
|
||||
{{{markdown this}}}
|
||||
{{~else~}}
|
||||
{{~#if (array_contains_case_insensitive ../../icon @key)~}}
|
||||
{{~icon_img this~}}
|
||||
{{~else~}}
|
||||
{{this}}
|
||||
{{~/if~}}
|
||||
{{~/if~}}
|
||||
</td>
|
||||
{{/if~}}
|
||||
{{~/each~}}
|
||||
{{#if ../edit_url}}
|
||||
<td class="align-middle _col_edit text-center">
|
||||
<a href="{{replace ../edit_url '{id}' _sqlpage_id}}" class="align-middle link-secondary _col_edit" data-action="edit" title="Edit">
|
||||
{{~icon_img 'edit'~}}
|
||||
</a>
|
||||
</td>
|
||||
{{/if}}
|
||||
{{#if ../delete_url}}
|
||||
<td class="align-middle _col_delete text-center">
|
||||
<a href="{{replace ../delete_url '{id}' _sqlpage_id}}" class="align-middle link-secondary _col_delete" data-action="delete" title="Delete">
|
||||
{{~icon_img 'trash'~}}
|
||||
</a>
|
||||
</td>
|
||||
{{/if}}
|
||||
{{#each (to_array ../custom_actions)}}
|
||||
<td class="align-middle _col_{{this.name}} text-center">
|
||||
<a href="{{replace this.link '{id}' ../_sqlpage_id}}" class="align-middle link-secondary _col_{{this.name}}" data-action="{{this.name}}" title="{{default this.tooltip this.name}}">{{!Title property sets the tooltip text}}
|
||||
{{~icon_img this.icon~}}
|
||||
</a>
|
||||
</td>
|
||||
{{/each}}
|
||||
|
||||
{{#each (to_array _sqlpage_actions)}}
|
||||
<td class="align-middle _col_{{this.name}} text-center">
|
||||
<a href="{{replace this.link '{id}' ../_sqlpage_id}}" class="align-middle link-secondary _col_{{this.name}}" data-action="{{this.name}}" title="{{default this.tooltip this.name}}">
|
||||
{{~icon_img this.icon~}}
|
||||
</a>
|
||||
</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
{{!~
|
||||
After this <tr> has been rendered, if this was a footer, we need to reopen a new <tbody>
|
||||
No need for another delayed closure since the previous one still applies
|
||||
~}}
|
||||
{{~#if _sqlpage_footer}} </tfoot><tbody class="table-tbody list"> {{/if~}}
|
||||
{{/each_row}}
|
||||
{{flush_delayed}}
|
||||
|
||||
{{! If not enough rows were rendered, we need to place a 'No data' cell. "Not enough rows" depends on the footer settings }}
|
||||
{{#if (eq @row_index 0)}}
|
||||
<tbody class="table-tbody list">
|
||||
<tr>
|
||||
<td class="text-center">{{default empty_description 'No data'}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{{/if}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
{{{~html~}}}
|
||||
{{~#if title~}}
|
||||
<h1 id="{{id}}" class="mt-3 {{#if center}}text-center{{/if}}">{{title}}</h1>
|
||||
{{~else~}}
|
||||
{{#if id}}
|
||||
<a id="{{id}}"></a>
|
||||
{{/if}}
|
||||
{{~/if~}}
|
||||
{{~#if contents_md~}}
|
||||
<div class="remove-bottom-margin {{#if center}}mx-auto{{/if}} {{#if article}}markdown article-text{{/if}}">
|
||||
{{{~markdown contents_md~}}}
|
||||
</div>
|
||||
{{~/if~}}
|
||||
{{~#if unsafe_contents_md~}}
|
||||
<div class="remove-bottom-margin {{#if center}}mx-auto{{/if}} {{#if article}}markdown article-text{{/if}}">
|
||||
{{{~markdown unsafe_contents_md 'allow_unsafe'~}}}
|
||||
</div>
|
||||
{{~/if~}}
|
||||
<p class="{{#if center}}mx-auto{{/if}} {{#if article}}markdown article-text{{/if}}">
|
||||
{{contents}}
|
||||
{{~#each_row~}}
|
||||
{{~#if break~}}</p><p>{{~/if~}}
|
||||
{{~#if link~}}
|
||||
<a href="{{link}}">{{#delay}}</a>{{/delay}}
|
||||
{{~/if~}}
|
||||
{{~#if code~}}
|
||||
<code>{{#delay}}</code>{{/delay}}
|
||||
{{~/if~}}
|
||||
<span class="
|
||||
{{~#if color}}text-{{color}} {{/if~}}
|
||||
{{~#if bold}}fw-bold {{/if~}}
|
||||
{{~#if italics}}fst-italic {{/if~}}
|
||||
{{~#if underline}}text-decoration-underline {{/if~}}
|
||||
{{~#if size}}fs-{{minus 7 size}} {{/if~}}
|
||||
">{{contents}}</span>
|
||||
{{~flush_delayed~}}
|
||||
{{~#if contents_md~}}
|
||||
{{{markdown contents_md}}}
|
||||
{{~/if~}}
|
||||
{{~#if unsafe_contents_md~}}
|
||||
{{{markdown unsafe_contents_md 'allow_unsafe'}}}
|
||||
{{~/if~}}
|
||||
{{~/each_row~}}
|
||||
</p>
|
||||
@@ -0,0 +1,31 @@
|
||||
<ul class="timeline {{#if simple}}timeline-simple{{/if}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
{{#each_row}}
|
||||
<li class="timeline-event {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<div class="timeline-event-icon {{#if color}}bg-{{color}}-lt{{/if}}">
|
||||
{{~icon_img (default icon 'git-commit')~}}
|
||||
</div>
|
||||
{{~#if link~}}
|
||||
<a class="card timeline-event-card" href="{{link}}">
|
||||
{{~else~}}
|
||||
<div class="card timeline-event-card">
|
||||
{{~/if~}}
|
||||
<div class="card-body">
|
||||
<div class="text-secondary float-end">{{date}}</div>
|
||||
{{~#if title}}<h4>{{title}}</h4>{{~/if~}}
|
||||
{{~#if (or description description_md)~}}
|
||||
<p class="text-secondary">
|
||||
{{~description~}}
|
||||
{{~#if description_md~}}
|
||||
{{{markdown description_md}}}
|
||||
{{~/if~}}
|
||||
</p>
|
||||
{{~/if~}}
|
||||
</div>
|
||||
{{~#if link~}}
|
||||
</a>
|
||||
{{~else~}}
|
||||
</div>
|
||||
{{~/if~}}
|
||||
</li>
|
||||
{{/each_row}}
|
||||
</ul>
|
||||
@@ -0,0 +1 @@
|
||||
<h{{default level 1}} class="mt-2 mb-1 {{#if center}}text-center{{/if}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>{{contents}}</h{{default level 1}}>
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="card my-2 col-md-{{default width 12}}{{#if center}} mx-auto{{/if}} {{class}}" {{#if id}}id="{{id}}"{{/if}}>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">{{title}}</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline remove-bottom-margin">
|
||||
<div class="h1 mb-3 me-2">{{#if information}}{{information}}{{/if}}</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline">
|
||||
<div class="mb-2 me-2 remove-bottom-margin">
|
||||
{{#if description_md}}
|
||||
{{{markdown description_md}}}
|
||||
{{else}}
|
||||
{{#if description}}{{description}}{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="tracking">
|
||||
{{#each_row}}
|
||||
<div class="tracking-block{{#if color}} bg-{{color}}{{/if}}" data-bs-toggle="tooltip" data-bs-placement="{{default ../placement "top"}}" title="{{title}}"></div>
|
||||
{{/each_row}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
/* !include https://cdn.jsdelivr.net/npm/tom-select@2.6.1/dist/js/tom-select.popular.min.js */
|
||||
|
||||
function sqlpage_select_dropdown() {
|
||||
for (const s of document.querySelectorAll(
|
||||
"[data-pre-init=select-dropdown]",
|
||||
)) {
|
||||
try {
|
||||
sqlpage_select_dropdown_individual(s);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a select dropdown for a single element
|
||||
* @param {HTMLSelectElement} s - The select element to initialize
|
||||
*/
|
||||
function sqlpage_select_dropdown_individual(s) {
|
||||
s.removeAttribute("data-pre-init");
|
||||
// See: https://github.com/orchidjs/tom-select/issues/716
|
||||
// By default, TomSelect will not retain the focus if s is already focused
|
||||
// This is a workaround to fix that
|
||||
const is_focused = s === document.activeElement;
|
||||
|
||||
const tom = new TomSelect(s, {
|
||||
load: sqlpage_load_options_source(s.dataset.options_source),
|
||||
valueField: "value",
|
||||
labelField: "label",
|
||||
searchField: "label",
|
||||
create: s.dataset.create_new,
|
||||
maxOptions: null,
|
||||
onItemAdd: function () {
|
||||
this.setTextboxValue("");
|
||||
this.refreshOptions();
|
||||
},
|
||||
});
|
||||
if (is_focused) tom.focus();
|
||||
s.form?.addEventListener("reset", async () => {
|
||||
// The reset event is fired before the form is reset, so we need to wait for the next event loop
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
// Sync the options with the new reset value
|
||||
tom.sync();
|
||||
// Wait for the options to be updated
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
// "sync" also focuses the input, so we need to blur it to remove the focus
|
||||
tom.blur();
|
||||
tom.close();
|
||||
});
|
||||
}
|
||||
|
||||
function sqlpage_load_options_source(options_source) {
|
||||
if (!options_source) return;
|
||||
return async (query, callback) => {
|
||||
const err = (label) => callback([{ label, value: "" }]);
|
||||
const resp = await fetch(
|
||||
`${options_source}?search=${encodeURIComponent(query)}`,
|
||||
);
|
||||
if (!resp.ok) {
|
||||
return err(
|
||||
`Error loading options from "${options_source}": ${resp.status} ${resp.statusText}`,
|
||||
);
|
||||
}
|
||||
const resp_type = resp.headers.get("content-type");
|
||||
if (resp_type !== "application/json") {
|
||||
return err(
|
||||
`Invalid response type: ${resp_type} from "${options_source}". Make sure to use the 'json' component in the SQL file that generates the options.`,
|
||||
);
|
||||
}
|
||||
const results = await resp.json();
|
||||
if (!Array.isArray(results)) {
|
||||
return err(
|
||||
`Invalid response from "${options_source}". The response must be an array of objects with a 'label' and a 'value' property.`,
|
||||
);
|
||||
}
|
||||
if (results.length === 1 && results[0].error) {
|
||||
return err(results[0].error);
|
||||
}
|
||||
if (results.length > 0) {
|
||||
const keys = Object.keys(results[0]);
|
||||
if (
|
||||
keys.length !== 2 ||
|
||||
!keys.includes("label") ||
|
||||
!keys.includes("value")
|
||||
) {
|
||||
return err(
|
||||
`Invalid response from "${options_source}". The response must be an array of objects with a 'label' and a 'value' property. Got: ${JSON.stringify(results[0])} in the first object instead.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
callback(results);
|
||||
};
|
||||
}
|
||||
add_init_fn(sqlpage_select_dropdown);
|
||||
Reference in New Issue
Block a user