Overhaul portal calculator frontend
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
onchange: (value: number) => void;
|
||||||
|
scale?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onchange,
|
||||||
|
scale = 100,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
function onInput(e: Event & { currentTarget: HTMLInputElement }) {
|
||||||
|
const pct = e.currentTarget.valueAsNumber;
|
||||||
|
if (!Number.isNaN(pct)) onchange(pct / scale);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="label text-xs">{label}</span>
|
||||||
|
<input
|
||||||
|
class="input input-sm w-20"
|
||||||
|
type="number"
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
|
{step}
|
||||||
|
{placeholder}
|
||||||
|
value={Math.round(value * scale)}
|
||||||
|
oninput={onInput}
|
||||||
|
{disabled}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
@@ -31,9 +31,15 @@
|
|||||||
const rawValue = standard.levels[lvl];
|
const rawValue = standard.levels[lvl];
|
||||||
const unit = allStandards.byActivity(activity).getMetadata().unit;
|
const unit = allStandards.byActivity(activity).getMetadata().unit;
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case "ms":
|
case "ms": {
|
||||||
const seconds = (rawValue / 1000).toFixed(1);
|
if (activity === Activity.Run) {
|
||||||
return `${seconds}`;
|
const totalSeconds = Math.round(rawValue / 1000);
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
return (rawValue / 1000).toFixed(1);
|
||||||
|
}
|
||||||
case "cm": {
|
case "cm": {
|
||||||
const totalInches = cmToIn(rawValue);
|
const totalInches = cmToIn(rawValue);
|
||||||
const feet = Math.floor(totalInches / 12);
|
const feet = Math.floor(totalInches / 12);
|
||||||
@@ -51,19 +57,9 @@
|
|||||||
<section class="w-full">
|
<section class="w-full">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-xs label">
|
<h2 class="text-xl font-semibold">
|
||||||
{#if selected.standardsConfig.params.activity[selected.activity].enableGeneration && allStandards
|
{allStandards.byActivity(selected.activity).getMetadata().name}
|
||||||
.byActivity(selected.activity)
|
</h2>
|
||||||
.getMetadata().generators.length}
|
|
||||||
(Generated data: {allStandards
|
|
||||||
.byActivity(selected.activity)
|
|
||||||
.getMetadata()
|
|
||||||
.generators.map((g) => g.metric)
|
|
||||||
.join(", ")})
|
|
||||||
{:else}
|
|
||||||
(Generated data: NONE)
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,12 @@
|
|||||||
} from "@blade-and-brawn/domain";
|
} from "@blade-and-brawn/domain";
|
||||||
import StandardsTable from "$lib/components/StandardsTable.svelte";
|
import StandardsTable from "$lib/components/StandardsTable.svelte";
|
||||||
import PlayersTable from "$lib/components/PlayersTable.svelte";
|
import PlayersTable from "$lib/components/PlayersTable.svelte";
|
||||||
|
import PercentInput from "$lib/components/PercentInput.svelte";
|
||||||
import { api } from "$lib/api";
|
import { api } from "$lib/api";
|
||||||
import { Value } from "@sinclair/typebox/value";
|
import { Value } from "@sinclair/typebox/value";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
type ConfigSummary = { id: string; name: string; datasetId: string };
|
type ConfigSummary = { id: string; name: string };
|
||||||
type DatasetSummary = { id: string; name: string };
|
type DatasetSummary = { id: string; name: string };
|
||||||
type Editor = {
|
type Editor = {
|
||||||
configId: string | null;
|
configId: string | null;
|
||||||
@@ -27,7 +28,11 @@
|
|||||||
params: StandardsParams;
|
params: StandardsParams;
|
||||||
data: StandardsData;
|
data: StandardsData;
|
||||||
};
|
};
|
||||||
type Snapshot = { name: string; datasetId: string; params: StandardsParams };
|
type Snapshot = {
|
||||||
|
name: string;
|
||||||
|
datasetId: string;
|
||||||
|
params: StandardsParams;
|
||||||
|
};
|
||||||
|
|
||||||
const DRAFT_KEY = "calculator-draft";
|
const DRAFT_KEY = "calculator-draft";
|
||||||
|
|
||||||
@@ -52,7 +57,9 @@
|
|||||||
|
|
||||||
const allStandards = $derived(
|
const allStandards = $derived(
|
||||||
editor
|
editor
|
||||||
? new Standards($state.snapshot({ data: editor.data, params: editor.params }))
|
? new Standards(
|
||||||
|
$state.snapshot({ data: editor.data, params: editor.params }),
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,11 +81,19 @@
|
|||||||
|
|
||||||
function errorMessage(err: unknown): string {
|
function errorMessage(err: unknown): string {
|
||||||
const value = (err as { value?: { error?: string } })?.value;
|
const value = (err as { value?: { error?: string } })?.value;
|
||||||
return value?.error ?? (err instanceof Error ? err.message : "Unknown error");
|
return (
|
||||||
|
value?.error ??
|
||||||
|
(err instanceof Error ? err.message : "Unknown error")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveDraft() {
|
function saveDraft() {
|
||||||
if (editor) localStorage.setItem(DRAFT_KEY, JSON.stringify(editor));
|
if (!editor) return;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(DRAFT_KEY, JSON.stringify(editor));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("failed to save draft", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshLists() {
|
async function refreshLists() {
|
||||||
@@ -92,8 +107,16 @@
|
|||||||
datasets = datasetsRes.data;
|
datasets = datasetsRes.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig(id: string, opt: { skipDirtyCheck?: boolean } = {}) {
|
async function loadConfig(
|
||||||
if (!opt.skipDirtyCheck && isDirty && !confirm("Discard unsaved changes?")) return;
|
id: string,
|
||||||
|
opt: { skipDirtyCheck?: boolean } = {},
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!opt.skipDirtyCheck &&
|
||||||
|
isDirty &&
|
||||||
|
!confirm("Discard unsaved changes?")
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
saveError = null;
|
saveError = null;
|
||||||
const res = await api.standards.configs({ id }).get();
|
const res = await api.standards.configs({ id }).get();
|
||||||
@@ -121,6 +144,7 @@
|
|||||||
|
|
||||||
async function onDatasetChange(datasetId: string) {
|
async function onDatasetChange(datasetId: string) {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
const target = editor;
|
||||||
saveError = null;
|
saveError = null;
|
||||||
const res = await api.standards.datasets({ id: datasetId }).get();
|
const res = await api.standards.datasets({ id: datasetId }).get();
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
@@ -128,37 +152,43 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Value.Assert(StandardsDataSchema, res.data.data);
|
Value.Assert(StandardsDataSchema, res.data.data);
|
||||||
|
// the user may have switched to a different config while this was in flight
|
||||||
|
if (editor !== target) return;
|
||||||
editor.datasetId = datasetId;
|
editor.datasetId = datasetId;
|
||||||
editor.data = res.data.data;
|
editor.data = res.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persist(mode: "update" | "create") {
|
async function persist(mode: "update" | "create") {
|
||||||
if (!editor || !editor.name) return;
|
if (!editor || !editor.name) return;
|
||||||
|
const target = editor;
|
||||||
saveError = null;
|
saveError = null;
|
||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
if (mode === "update" && editor.configId) {
|
if (mode === "update" && target.configId) {
|
||||||
const res = await api.standards.configs({ id: editor.configId }).put({
|
const res = await api.standards.configs({ id: target.configId }).put({
|
||||||
name: editor.name,
|
name: target.name,
|
||||||
datasetId: editor.datasetId,
|
datasetId: target.datasetId,
|
||||||
params: editor.params,
|
params: target.params,
|
||||||
});
|
});
|
||||||
if (res.error) throw res.error;
|
if (res.error) throw res.error;
|
||||||
} else {
|
} else {
|
||||||
const res = await api.standards.configs.post({
|
const res = await api.standards.configs.post({
|
||||||
name: editor.name,
|
name: target.name,
|
||||||
datasetId: editor.datasetId,
|
datasetId: target.datasetId,
|
||||||
params: editor.params,
|
params: target.params,
|
||||||
});
|
});
|
||||||
if (res.error) throw res.error;
|
if (res.error) throw res.error;
|
||||||
editor.configId = res.data.id;
|
// only stamp the new id if the user hasn't switched away in the meantime
|
||||||
|
if (editor === target) editor.configId = res.data.id;
|
||||||
}
|
}
|
||||||
|
if (editor === target) {
|
||||||
savedSnapshot = {
|
savedSnapshot = {
|
||||||
name: editor.name,
|
name: target.name,
|
||||||
datasetId: editor.datasetId,
|
datasetId: target.datasetId,
|
||||||
params: $state.snapshot(editor.params),
|
params: $state.snapshot(target.params),
|
||||||
};
|
};
|
||||||
saveDraft();
|
saveDraft();
|
||||||
|
}
|
||||||
await refreshLists();
|
await refreshLists();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
saveError = errorMessage(err);
|
saveError = errorMessage(err);
|
||||||
@@ -182,14 +212,15 @@
|
|||||||
|
|
||||||
async function setLive() {
|
async function setLive() {
|
||||||
if (!editor?.configId || isDirty) return;
|
if (!editor?.configId || isDirty) return;
|
||||||
|
const configId = editor.configId;
|
||||||
saveError = null;
|
saveError = null;
|
||||||
settingLive = true;
|
settingLive = true;
|
||||||
try {
|
try {
|
||||||
const res = await api.calculator.standards.config.switch.post({
|
const res = await api.calculator.standards.config.switch.post({
|
||||||
standardsConfigId: editor.configId,
|
standardsConfigId: configId,
|
||||||
});
|
});
|
||||||
if (res.error) throw res.error;
|
if (res.error) throw res.error;
|
||||||
liveConfigId = editor.configId;
|
liveConfigId = configId;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
saveError = errorMessage(err);
|
saveError = errorMessage(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -197,31 +228,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
async function restoreDraft(): Promise<boolean> {
|
||||||
try {
|
|
||||||
await refreshLists();
|
|
||||||
|
|
||||||
const liveRes = await api.calculator.standards.config.get();
|
|
||||||
if (liveRes.error) throw liveRes.error;
|
|
||||||
Value.Assert(StandardsParamsSchema, liveRes.data.params);
|
|
||||||
Value.Assert(StandardsDataSchema, liveRes.data.data);
|
|
||||||
liveConfigId = liveRes.data.id;
|
|
||||||
|
|
||||||
const draftStringified = localStorage.getItem(DRAFT_KEY);
|
const draftStringified = localStorage.getItem(DRAFT_KEY);
|
||||||
if (draftStringified) {
|
if (!draftStringified) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
const draft = JSON.parse(draftStringified) as Editor;
|
const draft = JSON.parse(draftStringified) as Editor;
|
||||||
editor = draft;
|
Value.Assert(StandardsParamsSchema, draft.params);
|
||||||
|
Value.Assert(StandardsDataSchema, draft.data);
|
||||||
|
|
||||||
if (draft.configId) {
|
if (draft.configId) {
|
||||||
const configRes = await api.standards.configs({ id: draft.configId }).get();
|
const configRes = await api.standards.configs({ id: draft.configId }).get();
|
||||||
if (!configRes.error) {
|
if (configRes.error) throw configRes.error;
|
||||||
savedSnapshot = {
|
savedSnapshot = {
|
||||||
name: configRes.data.name,
|
name: configRes.data.name,
|
||||||
datasetId: configRes.data.datasetId,
|
datasetId: configRes.data.datasetId,
|
||||||
params: configRes.data.params,
|
params: configRes.data.params,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editor = draft;
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// stale/incompatible/dangling draft (schema drift, deleted config, corrupted JSON) - drop it
|
||||||
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
|
saveError =
|
||||||
|
"Your saved draft could not be restored, so the live config was loaded instead.";
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const [, liveRes] = await Promise.all([
|
||||||
|
refreshLists(),
|
||||||
|
api.calculator.standards.config.get(),
|
||||||
|
]);
|
||||||
|
if (liveRes.error) throw liveRes.error;
|
||||||
|
Value.Assert(StandardsParamsSchema, liveRes.data.params);
|
||||||
|
Value.Assert(StandardsDataSchema, liveRes.data.data);
|
||||||
|
liveConfigId = liveRes.data.id;
|
||||||
|
|
||||||
|
if (!(await restoreDraft())) {
|
||||||
await loadConfig(liveRes.data.id, { skipDirtyCheck: true });
|
await loadConfig(liveRes.data.id, { skipDirtyCheck: true });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -246,9 +294,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{@const currentEditor = editor}
|
{@const currentEditor = editor}
|
||||||
|
{@const activityParams = currentEditor.params.activity[activity]}
|
||||||
|
{@const activityMetadata = allStandards.byActivity(activity).getMetadata()}
|
||||||
<div
|
<div
|
||||||
class="w-full -mt-10 -mx-10 px-6 py-2 bg-base-200 border-b border-base-300 flex items-center gap-2 flex-wrap text-sm"
|
class="w-full -mt-10 -mx-10 px-6 py-2 bg-base-200 border-b border-base-300 flex items-center gap-2 flex-wrap text-sm"
|
||||||
>
|
>
|
||||||
|
<span class="label">Configuration</span>
|
||||||
<select
|
<select
|
||||||
class="select select-sm"
|
class="select select-sm"
|
||||||
value={currentEditor.configId ?? ""}
|
value={currentEditor.configId ?? ""}
|
||||||
@@ -256,20 +307,30 @@
|
|||||||
>
|
>
|
||||||
{#each configs as config}
|
{#each configs as config}
|
||||||
<option value={config.id}>
|
<option value={config.id}>
|
||||||
{config.name || "(untitled)"}{config.id === liveConfigId ? " (live)" : ""}
|
{config.name || "(untitled)"}{config.id === liveConfigId
|
||||||
|
? " (live)"
|
||||||
|
: ""}
|
||||||
</option>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{#if isDirty}
|
||||||
|
<span class="badge badge-warning badge-sm">Unsaved</span>
|
||||||
|
{:else if currentEditor.configId === liveConfigId}
|
||||||
|
<span class="badge badge-success badge-sm">Live</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<details class="dropdown">
|
<details class="dropdown">
|
||||||
<summary
|
<summary
|
||||||
class="btn btn-ghost btn-sm gap-1 font-normal opacity-70 list-none [&::-webkit-details-marker]:hidden"
|
class="btn btn-ghost btn-sm gap-1 font-normal opacity-70 list-none [&::-webkit-details-marker]:hidden"
|
||||||
>
|
>
|
||||||
Edit ▾
|
More ▾
|
||||||
</summary>
|
</summary>
|
||||||
<div class="dropdown-content z-10 menu bg-base-100 rounded-box shadow p-4 gap-3 w-64 mt-2">
|
<div
|
||||||
|
class="dropdown-content z-10 menu bg-base-100 rounded-box shadow p-4 gap-3 w-64 mt-2"
|
||||||
|
>
|
||||||
<label class="w-full">
|
<label class="w-full">
|
||||||
<span class="label mb-1">Name</span>
|
<span class="label mb-1">Configuration name</span>
|
||||||
<input
|
<input
|
||||||
class="input input-sm w-full"
|
class="input input-sm w-full"
|
||||||
bind:value={currentEditor.name}
|
bind:value={currentEditor.name}
|
||||||
@@ -291,60 +352,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
{#if isDirty}
|
|
||||||
<span class="badge badge-warning badge-sm">Unsaved</span>
|
|
||||||
{:else if currentEditor.configId === liveConfigId}
|
|
||||||
<span class="badge badge-success badge-sm">Live</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|
||||||
<button class="btn btn-ghost btn-sm" disabled={!isDirty} onclick={discard}>
|
<button
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
disabled={!isDirty}
|
||||||
|
onclick={discard}
|
||||||
|
>
|
||||||
Discard
|
Discard
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
disabled={settingLive || isDirty || currentEditor.configId === liveConfigId}
|
disabled={settingLive ||
|
||||||
|
isDirty ||
|
||||||
|
currentEditor.configId === liveConfigId}
|
||||||
onclick={setLive}
|
onclick={setLive}
|
||||||
>
|
>
|
||||||
{settingLive ? "Setting live..." : "Set as live"}
|
{settingLive ? "Setting live..." : "Set as live"}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline btn-sm" disabled={saving || !currentEditor.name} onclick={saveAsNew}>
|
<button
|
||||||
|
class="btn btn-outline btn-sm"
|
||||||
|
disabled={saving || !currentEditor.name}
|
||||||
|
onclick={saveAsNew}
|
||||||
|
>
|
||||||
Save as new
|
Save as new
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary btn-sm" disabled={saving || !isDirty} onclick={save}>
|
<button
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
disabled={saving || !isDirty}
|
||||||
|
onclick={save}
|
||||||
|
>
|
||||||
{saving ? "Saving..." : "Save"}
|
{saving ? "Saving..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if saveError}
|
{#if saveError}
|
||||||
<div role="alert" class="alert alert-error w-full mt-4"><span>{saveError}</span></div>
|
<div role="alert" class="alert alert-error w-full mt-4">
|
||||||
|
<span>{saveError}</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<section class="w-full flex flex-col gap-4 mt-6">
|
<section class="w-full flex gap-4 flex-wrap mt-6">
|
||||||
<div class="flex justify-between gap-2 items-center flex-wrap">
|
<fieldset class="fieldset bg-base-200 p-4">
|
||||||
<fieldset class="fieldset bg-base-200 p-6 max-w-lg">
|
<legend class="fieldset-legend text-lg font-semibold"
|
||||||
<legend class="fieldset-legend text-lg font-semibold">Data</legend>
|
>Global Parameters</legend
|
||||||
|
>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<label class="flex flex-col gap-1">
|
||||||
<fieldset class="fieldset bg-base-300 p-3 max-w-xl">
|
<span class="label text-xs">Max level</span>
|
||||||
<legend class="fieldset-legend text-sm font-semibold">General</legend>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4">
|
|
||||||
<label>
|
|
||||||
<span class="label mb-1">Activity</span>
|
|
||||||
<select class="select w-full" name="activities" bind:value={activity}>
|
|
||||||
{#each Object.values(Activity) as a}
|
|
||||||
<option value={a}>
|
|
||||||
{allStandards.byActivity(a).getMetadata().name}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span class="label mb-1">Max level</span>
|
|
||||||
<input
|
<input
|
||||||
class="input w-full"
|
class="input input-sm w-36"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
@@ -352,142 +409,116 @@
|
|||||||
bind:value={currentEditor.params.global.maxLevel}
|
bind:value={currentEditor.params.global.maxLevel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset bg-base-300 p-3 max-w-3xs">
|
<fieldset class="fieldset bg-base-200 p-4">
|
||||||
<legend class="fieldset-legend text-sm font-semibold">Data Generation</legend>
|
<legend class="fieldset-legend text-lg font-semibold"
|
||||||
|
>{activityMetadata.name} Parameters</legend
|
||||||
|
>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<label class="flex flex-col gap-1 mb-3">
|
||||||
<label class="label">
|
<span class="label text-xs">Activity</span>
|
||||||
<input
|
<select
|
||||||
type="checkbox"
|
class="select select-sm w-36"
|
||||||
bind:checked={currentEditor.params.activity[activity].enableGeneration}
|
name="activities"
|
||||||
class="toggle toggle-lg m-auto"
|
bind:value={activity}
|
||||||
/>
|
>
|
||||||
</label>
|
{#each Object.values(Activity) as a}
|
||||||
<label>
|
<option value={a}>
|
||||||
<span class="label mb-1">Weight significance</span>
|
{allStandards.byActivity(a).getMetadata().name}
|
||||||
<input
|
</option>
|
||||||
class="input w-full"
|
|
||||||
type="number"
|
|
||||||
min="-1"
|
|
||||||
max="1"
|
|
||||||
step=".05"
|
|
||||||
placeholder=".1"
|
|
||||||
bind:value={currentEditor.params.activity[activity].weightModifier}
|
|
||||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span class="label mb-1">Difficulty modifier</span>
|
|
||||||
<input
|
|
||||||
class="input w-full"
|
|
||||||
type="number"
|
|
||||||
min="-1"
|
|
||||||
max="1"
|
|
||||||
step=".05"
|
|
||||||
bind:value={currentEditor.params.activity[activity].difficultyModifier}
|
|
||||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span class="label mb-1">Age significance</span>
|
|
||||||
<input
|
|
||||||
class="input w-full"
|
|
||||||
type="number"
|
|
||||||
min="-1"
|
|
||||||
max="1"
|
|
||||||
step=".05"
|
|
||||||
bind:value={currentEditor.params.activity[activity].ageModifier}
|
|
||||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span class="label mb-1">Peak age</span>
|
|
||||||
<input
|
|
||||||
class="input w-full"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
step="1"
|
|
||||||
bind:value={currentEditor.params.activity[activity].peakAge}
|
|
||||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<fieldset class="fieldset bg-base-200 p-3 max-w-xl">
|
|
||||||
<legend class="fieldset-legend text-sm font-semibold">Stretch</legend>
|
|
||||||
<label>
|
|
||||||
<span class="label mb-1">Lower</span>
|
|
||||||
<input
|
|
||||||
class="input w-full"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="10"
|
|
||||||
step="1"
|
|
||||||
bind:value={currentEditor.params.activity[activity].stretch.lower}
|
|
||||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span class="label mb-1">Upper</span>
|
|
||||||
<input
|
|
||||||
class="input w-full"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="10"
|
|
||||||
step="1"
|
|
||||||
bind:value={currentEditor.params.activity[activity].stretch.upper}
|
|
||||||
disabled={!currentEditor.params.activity[activity].enableGeneration}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
{#if activeTab === "standards"}
|
|
||||||
<fieldset class="fieldset bg-base-200 p-6 max-w-lg">
|
|
||||||
<legend class="fieldset-legend text-lg font-semibold">Metrics</legend>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<label>
|
|
||||||
<span class="label mb-1">Gender</span>
|
|
||||||
<select class="select w-full" name="genders" bind:value={metrics.gender}>
|
|
||||||
{#each Object.values(Gender) as gender}
|
|
||||||
<option value={gender}>{gender}</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<div class="flex gap-3 flex-wrap">
|
||||||
<span class="label mb-1">Age</span>
|
<fieldset class="fieldset bg-base-300 p-2">
|
||||||
<input
|
<legend class="fieldset-legend text-sm font-semibold"
|
||||||
class="input w-full"
|
>Modifiers</legend
|
||||||
type="number"
|
>
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
placeholder="18"
|
|
||||||
bind:value={metrics.age}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
<PercentInput
|
||||||
<span class="label mb-1">Weight (lb)</span>
|
label="Difficulty (%)"
|
||||||
|
min={-100}
|
||||||
|
max={100}
|
||||||
|
step={5}
|
||||||
|
value={activityParams.difficultyModifier}
|
||||||
|
onchange={(v) => (activityParams.difficultyModifier = v)}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset bg-base-300 p-2">
|
||||||
|
<legend class="fieldset-legend text-sm font-semibold"
|
||||||
|
>Generation</legend
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="label text-xs">Enabled</span>
|
||||||
<input
|
<input
|
||||||
class="input w-full"
|
type="checkbox"
|
||||||
type="number"
|
bind:checked={activityParams.enableGeneration}
|
||||||
min="0"
|
class="toggle toggle-sm self-start mt-1.5"
|
||||||
max="600"
|
/>
|
||||||
placeholder="170"
|
</label>
|
||||||
bind:value={metrics.weight}
|
<PercentInput
|
||||||
|
label="Stretch lower (%)"
|
||||||
|
scale={10}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={10}
|
||||||
|
value={activityParams.stretch.lower}
|
||||||
|
onchange={(v) => (activityParams.stretch.lower = v)}
|
||||||
|
disabled={!activityParams.enableGeneration}
|
||||||
|
/>
|
||||||
|
<PercentInput
|
||||||
|
label="Stretch upper (%)"
|
||||||
|
scale={10}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={10}
|
||||||
|
value={activityParams.stretch.upper}
|
||||||
|
onchange={(v) => (activityParams.stretch.upper = v)}
|
||||||
|
disabled={!activityParams.enableGeneration}
|
||||||
|
/>
|
||||||
|
{#if activityMetadata.generators.some((g) => g.metric === "weight")}
|
||||||
|
<PercentInput
|
||||||
|
label="Weight sig. (%)"
|
||||||
|
min={-100}
|
||||||
|
max={100}
|
||||||
|
step={5}
|
||||||
|
value={activityParams.weightModifier}
|
||||||
|
onchange={(v) => (activityParams.weightModifier = v)}
|
||||||
|
disabled={!activityParams.enableGeneration}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if activityMetadata.generators.some((g) => g.metric === "age")}
|
||||||
|
<PercentInput
|
||||||
|
label="Age sig. (%)"
|
||||||
|
min={-100}
|
||||||
|
max={100}
|
||||||
|
step={5}
|
||||||
|
value={activityParams.ageModifier}
|
||||||
|
onchange={(v) => (activityParams.ageModifier = v)}
|
||||||
|
disabled={!activityParams.enableGeneration}
|
||||||
|
/>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="label text-xs">Peak age</span>
|
||||||
|
<input
|
||||||
|
class="input input-sm w-20"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
bind:value={activityParams.peakAge}
|
||||||
|
disabled={!activityParams.enableGeneration}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
@@ -513,11 +544,52 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if activeTab === "standards"}
|
{#if activeTab === "standards"}
|
||||||
|
<div class="flex items-center gap-4 text-sm opacity-70 mt-6">
|
||||||
|
<span>Filter:</span>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<span class="label">Gender</span>
|
||||||
|
<select
|
||||||
|
class="select select-sm w-28"
|
||||||
|
name="genders"
|
||||||
|
bind:value={metrics.gender}
|
||||||
|
>
|
||||||
|
{#each Object.values(Gender) as gender}
|
||||||
|
<option value={gender}>{gender}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<span class="label">Age</span>
|
||||||
|
<input
|
||||||
|
class="input input-sm w-20"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
placeholder="18"
|
||||||
|
bind:value={metrics.age}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<span class="label">Weight (lb)</span>
|
||||||
|
<input
|
||||||
|
class="input input-sm w-20"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="600"
|
||||||
|
placeholder="170"
|
||||||
|
bind:value={metrics.weight}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<StandardsTable
|
<StandardsTable
|
||||||
selected={{
|
selected={{
|
||||||
activity,
|
activity,
|
||||||
metrics: selectedMetrics,
|
metrics: selectedMetrics,
|
||||||
standardsConfig: { data: currentEditor.data, params: currentEditor.params },
|
standardsConfig: {
|
||||||
|
data: currentEditor.data,
|
||||||
|
params: currentEditor.params,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
{allStandards}
|
{allStandards}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user