From 8a2d946b633548d93b9421edc7d70f0a5916eda9 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 11 May 2025 17:24:09 -0600 Subject: [PATCH] Create the initial version of the TableManager class for configuring settings --- module/Apps/StatSidebar.mjs | 9 +- module/Apps/StatsViewer.mjs | 23 +- module/Apps/TableCreator.mjs | 2 +- module/Apps/TableManager.mjs | 236 ++++++++++++++++++ module/api.mjs | 5 +- module/hooks/init.mjs | 19 +- module/utils/buckets.mjs | 123 +++++++++ module/utils/databases/Memory.mjs | 78 +++++- module/utils/validateValue.mjs | 73 ------ public/styles/Apps/StatsViewer.css | 16 -- public/styles/Apps/TableManager.css | 63 +++++ public/styles/Apps/common.css | 21 ++ public/styles/elements/span.css | 5 + public/styles/elements/string-tags.css | 13 + public/styles/main.css | 9 +- .../Apps/TableManager/buckets/empty.hbs | 7 + .../Apps/TableManager/buckets/number.hbs | 57 +++++ .../Apps/TableManager/buckets/range.hbs | 50 ++++ .../Apps/TableManager/buckets/string.hbs | 17 ++ public/templates/Apps/TableManager/submit.hbs | 5 + public/templates/Apps/common/tableSelect.hbs | 2 + 21 files changed, 718 insertions(+), 115 deletions(-) create mode 100644 module/Apps/TableManager.mjs create mode 100644 module/utils/buckets.mjs delete mode 100644 module/utils/validateValue.mjs create mode 100644 public/styles/Apps/TableManager.css create mode 100644 public/styles/Apps/common.css create mode 100644 public/styles/elements/span.css create mode 100644 public/styles/elements/string-tags.css create mode 100644 public/templates/Apps/TableManager/buckets/empty.hbs create mode 100644 public/templates/Apps/TableManager/buckets/number.hbs create mode 100644 public/templates/Apps/TableManager/buckets/range.hbs create mode 100644 public/templates/Apps/TableManager/buckets/string.hbs create mode 100644 public/templates/Apps/TableManager/submit.hbs diff --git a/module/Apps/StatSidebar.mjs b/module/Apps/StatSidebar.mjs index af87f61..e6fbe05 100644 --- a/module/Apps/StatSidebar.mjs +++ b/module/Apps/StatSidebar.mjs @@ -15,6 +15,7 @@ export class StatSidebar extends HandlebarsApplicationMixin(AbstractSidebarTab) }, actions: { openStats: this.#openStats, + manageTables: this.#manageTables, createTable: this.#createTable, }, }; @@ -39,7 +40,7 @@ export class StatSidebar extends HandlebarsApplicationMixin(AbstractSidebarTab) const controls = { openStats: { label: `View Stats`, action: `openStats` }, createTable: { label: `Create New Table`, action: `createTable` }, - manageTables: { label: `Manage Tables`, action: `` }, + manageTables: { label: `Manage Tables`, action: `manageTables` }, manageData: { label: `Manage Data`, action: `` }, }; @@ -65,6 +66,12 @@ export class StatSidebar extends HandlebarsApplicationMixin(AbstractSidebarTab) app.render({ force: true }); }; + /** @this {StatSidebar} */ + static async #manageTables() { + const app = new CONFIG.stats.manager; + app.render({ force: true }); + }; + /** @this {StatSidebar} */ static async #createTable() { const app = new CONFIG.stats.creator; diff --git a/module/Apps/StatsViewer.mjs b/module/Apps/StatsViewer.mjs index 03ae858..5568d7f 100644 --- a/module/Apps/StatsViewer.mjs +++ b/module/Apps/StatsViewer.mjs @@ -117,8 +117,16 @@ export class StatsViewer extends HandlebarsApplicationMixin(ApplicationV2) { return ctx; }; - _selectedTable; - _selectedSubtable; + #_selectedTable = ``; + _selectedSubtable = ``; + get _selectedTable() { + return this.#_selectedTable; + }; + set _selectedTable(val) { + this.#_selectedTable = val; + this._selectedSubtable = ``; + }; + async #prepareTableSelectContext(ctx) { const tables = new Set(); const subtables = {}; @@ -264,15 +272,12 @@ export class StatsViewer extends HandlebarsApplicationMixin(ApplicationV2) { async #bindListener(event) { const target = event.target; const data = target.dataset; - const binding = data.bind; - if (!binding || !Object.hasOwn(this, binding)) { - Logger.debug(`Skipping change for element with binding "${binding}"`); - return; - }; - Logger.log(`updating ${binding} value to ${target.value}`); - this[binding] = target.value; + if (import.meta.env.DEV) { + Logger.debug(`updating ${binding} value to ${target.value}`); + } + Reflect.set(this, binding, target.value); this.render(); }; diff --git a/module/Apps/TableCreator.mjs b/module/Apps/TableCreator.mjs index 0eb44e5..5be09e3 100644 --- a/module/Apps/TableCreator.mjs +++ b/module/Apps/TableCreator.mjs @@ -1,4 +1,4 @@ -import { BucketTypes } from "../utils/validateValue.mjs"; +import { BucketTypes } from "../utils/buckets.mjs"; import { createDiceTable } from "../utils/databases/utils.mjs"; import { filePath } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; diff --git a/module/Apps/TableManager.mjs b/module/Apps/TableManager.mjs new file mode 100644 index 0000000..9df5b90 --- /dev/null +++ b/module/Apps/TableManager.mjs @@ -0,0 +1,236 @@ +import { diceSizeSorter } from "../utils/sorters/diceSize.mjs"; +import { filePath } from "../consts.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { smallToLarge } from "../utils/sorters/smallToLarge.mjs"; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +const { isEmpty } = foundry.utils; + +export class TableManager extends HandlebarsApplicationMixin(ApplicationV2) { + // #region Options + static DEFAULT_OPTIONS = { + tag: `form`, + classes: [ + __ID__, + `TableManager`, + ], + window: { + title: `Table Manager`, + frame: true, + positioned: true, + resizable: true, + minimizable: true, + contentClasses: [`st-scrollable`], + controls: [ + // Add action for deleting the table + ], + }, + position: { + width: 475, + height: 440, + }, + form: { + submitOnChange: false, + closeOnSubmit: false, + handler: this.#submit, + }, + actions: {}, + }; + + static PARTS = { + tableSelect: { + template: filePath(`templates/Apps/common/tableSelect.hbs`), + }, + buckets: { + template: filePath(`templates/Apps/TableManager/buckets/empty.hbs`), + }, + submit: { + template: filePath(`templates/Apps/TableManager/submit.hbs`), + }, + }; + + _configureRenderOptions(options) { + const table = CONFIG.stats.db.getTable(this.activeTableID); + + let bucketType = table?.buckets?.type ?? `empty`; + this.constructor.PARTS.buckets = { + template: filePath(`templates/Apps/TableManager/buckets/${bucketType}.hbs`), + }; + + super._configureRenderOptions(options); + }; + // #endregion Options + + // #region Selected Table + #_selectedTable = ``; + _selectedSubtable = ``; + get _selectedTable() { + return this.#_selectedTable; + }; + set _selectedTable(val) { + this.#_selectedTable = val; + this._selectedSubtable = ``; + }; + + get activeTableID() { + if (this._selectedSubtable) { + return `${this._selectedTable}/${this._selectedSubtable}`; + } + return this._selectedTable; + }; + // #endregion Selected Table + + // #region Lifecycle + async render({ userUpdated, ...opts } = {}) { + if (userUpdated) { + return; + } + await super.render(opts); + }; + + async _onRender(context, options) { + await super._onRender(context, options); + + const elements = this.element + .querySelectorAll(`[data-bind]`); + for (const input of elements) { + input.addEventListener(`change`, this.#bindListener.bind(this)); + }; + }; + + async _onFirstRender(context, options) { + await super._onFirstRender(context, options); + CONFIG.stats.db.addApp(this); + }; + + _tearDown() { + CONFIG.stats.db.removeApp(this); + return super._tearDown(); + }; + // #endregion Lifecycle + + // #region Data Prep + async _preparePartContext(partId) { + const ctx = { + table: this._selectedTable, + subtable: this._selectedSubtable, + }; + ctx.meta = { + idp: this.id, + }; + + switch (partId) { + case `tableSelect`: { + await this.#prepareTableSelectContext(ctx); + break; + }; + case `buckets`: { + await this.#prepareBucketContext(ctx); + break; + }; + }; + + if (import.meta.env.DEV) { + Logger.log(partId, `context`, ctx); + }; + return ctx; + }; + + async #prepareTableSelectContext(ctx) { + const tables = new Set(); + const subtables = {}; + + for (const tableConfig of CONFIG.stats.db.getTables()) { + const [ table, subtable ] = tableConfig.name.split(`/`); + tables.add(table); + if (subtable?.length > 0) { + subtables[table] ??= []; + subtables[table].push(subtable); + }; + }; + + const tableList = Array.from(tables); + ctx.table = this._selectedTable; + ctx.tables = tableList; + + const subtableList = subtables[this._selectedTable]; + + // Sort the subtables to be sane + if (this._selectedTable === `Dice`) { + subtableList?.sort(diceSizeSorter); + } else { + subtableList?.sort(smallToLarge); + }; + + ctx.subtable = this._selectedSubtable; + ctx.subtables = subtableList; + }; + + async #prepareBucketContext(ctx) { + const table = CONFIG.stats.db.getTable(this.activeTableID); + if (!table) { return }; + const type = table.buckets.type; + const capitalizedType = type[0].toUpperCase() + type.slice(1); + if (!this[`_prepare${capitalizedType}Context`]) { return }; + this[`_prepare${capitalizedType}Context`](ctx, table); + }; + + async _prepareNumberContext(ctx, table) { + ctx.buckets = { + min: table.buckets.min, + max: table.buckets.max, + step: table.buckets.step, + }; + }; + + async _prepareRangeContext(ctx, table) { + ctx.buckets = { + locked: this._selectedTable === `Dice` || table.buckets.locked, + min: table.buckets.min, + max: table.buckets.max, + step: table.buckets.step, + }; + }; + + async _prepareStringContext(ctx, table) { + ctx.buckets = { + choices: [...table.buckets.choices], + }; + }; + // #endregion Data Prep + + // #region Actions + /** + * @param {Event} event + */ + async #bindListener(event) { + const target = event.target; + const data = target.dataset; + const binding = data.bind; + + if (import.meta.env.DEV) { + Logger.debug(`updating ${binding} value to ${target.value}`); + } + Reflect.set(this, binding, target.value); + this.render(); + }; + + /** + * Process form submission for the sheet. + * @this {DocumentSheetV2} The handler is called with the application as its bound scope + * @param {SubmitEvent} event The originating form submission event + * @param {HTMLFormElement} form The form element that was submitted + * @param {FormDataExtended} formData Processed data for the submitted form + * @param {object} [options] Additional options provided by a manual submit call. All options except `options.updateData` are forwarded along to _processSubmitData. + * @param {object} [options.updateData] Additional data passed in if this form is submitted manually which should be merged with prepared formData. + * @returns {Promise} + */ + static async #submit(_event, _form, formData, _options) { + if (isEmpty(formData.object)) { + ui.notifications.info(`Nothing to save`); + return; + } + CONFIG.stats.db.updateTable(this.activeTableID, formData.object); + }; + // #endregion Actions +}; diff --git a/module/api.mjs b/module/api.mjs index a6587a0..a54c23c 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -1,11 +1,12 @@ // Apps import { StatsViewer } from "./Apps/StatsViewer.mjs"; import { TableCreator } from "./Apps/TableCreator.mjs"; +import { TableManager } from "./Apps/TableManager.mjs"; import { TestApp } from "./Apps/TestApp.mjs"; // Utils +import { validateBucketConfig, validateValue } from "./utils/buckets.mjs"; import { filterPrivateRows } from "./utils/privacy.mjs"; -import { validateValue } from "./utils/validateValue.mjs"; const { deepFreeze } = foundry.utils; @@ -18,10 +19,12 @@ Object.defineProperty( TestApp, StatsViewer, TableCreator, + TableManager, }, utils: { filterPrivateRows, validateValue, + validateBucketConfig, }, }), writable: false, diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 1c5ace9..70dfad6 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -1,13 +1,19 @@ -import helpers from "../handlebarsHelpers/_index.mjs"; -import { Logger } from "../utils/Logger.mjs"; +// Databases import { MemoryDatabase } from "../utils/databases/Memory.mjs"; -import { registerCustomComponents } from "../Apps/elements/_index.mjs"; -import { registerMetaSettings } from "../settings/meta.mjs"; -import { registerWorldSettings } from "../settings/world.mjs"; +import { UserFlagDatabase } from "../utils/databases/UserFlag.mjs"; + +// Applications import { StatSidebar } from "../Apps/StatSidebar.mjs"; import { StatsViewer } from "../Apps/StatsViewer.mjs"; import { TableCreator } from "../Apps/TableCreator.mjs"; -import { UserFlagDatabase } from "../utils/databases/UserFlag.mjs"; +import { TableManager } from "../Apps/TableManager.mjs"; + +// Misc Imports +import helpers from "../handlebarsHelpers/_index.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { registerCustomComponents } from "../Apps/elements/_index.mjs"; +import { registerMetaSettings } from "../settings/meta.mjs"; +import { registerWorldSettings } from "../settings/world.mjs"; Hooks.on(`init`, () => { Logger.debug(`Initializing`); @@ -32,6 +38,7 @@ Hooks.on(`init`, () => { db: UserFlagDatabase, viewer: StatsViewer, creator: TableCreator, + manager: TableManager, }; if (import.meta.env.DEV) { diff --git a/module/utils/buckets.mjs b/module/utils/buckets.mjs new file mode 100644 index 0000000..c689445 --- /dev/null +++ b/module/utils/buckets.mjs @@ -0,0 +1,123 @@ +import { Logger } from "./Logger.mjs"; + +const { deepClone } = foundry.utils; +const { StringField, NumberField } = foundry.data.fields; + +export const BucketTypes = { + STRING: `string`, + NUMBER: `number`, + RANGE: `range`, +}; + +/** + * @param {unknown} value The value to validate + * @param {BucketConfig} options The bucket config for the table + * @returns Whether or not the value is valid for the table + */ +export function validateValue(value, options) { + /** @type {BucketConfig} */ + let opts = deepClone(options); + + const validator = validators[opts.type]; + if (validator == null) { + Logger.error(`Failed to find type validator for: ${opts.type}`); + return false; + }; + + // Disallow function choices if present + if (typeof opts.choices === `function`) { + delete opts.choices; + }; + + // Get ride of properties that aren't part of the data fields + delete opts.type; + delete opts.locked; + + validator.transformOptions(opts); + + const field = new validator.field(opts); + const error = field.validate(value); + + // DataFields return a class instance on error, or void when valid. + return !error; +}; + +/** + * @param {BucketConfig} config The bucket config for the table + * @returns {BucketConfig} The transformed bucket config + */ +export function validateBucketConfig(config) { + /** @type {BucketConfig} */ + let conf = deepClone(config); + + const validator = validators[conf.type]; + if (validator == null) { + Logger.error(`Failed to find type validator for: ${conf.type}`); + return false; + }; + + // Disallow function choices if present + if (typeof conf.choices === `function`) { + Logger.error(`Choices cannot be a function in a table's buckets configuraion`); + delete conf.choices; + }; + + validator.validateConfig(conf); + + return conf; +}; + +const validators = { + [BucketTypes.STRING]: { + field: StringField, + transformOptions: (opts) => { + opts.nullable = false; + opts.trim = true; + opts.blank = false; + }, + validateConfig: (config) => { + if (config.choices.length === 0) { + delete config.choices; + config[`-=choices`] = null; + }; + }, + }, + [BucketTypes.NUMBER]: { + field: NumberField, + transformOptions: transformNumberFieldOptions, + validateConfig: (config) => { + if (config.step != null && config.min == null) { + delete config.step; + config[`-=step`] = null; + }; + if ( + config.min != null + && config.max != null + && config.min > config.max + ) { + throw new Error(`"min" must be less than "max"`); + } + }, + }, + [BucketTypes.RANGE]: { + field: NumberField, + transformOptions: transformNumberFieldOptions, + validateConfig: (config) => { + if (config.min == null) { + throw new Error(`"min" must be defined for range buckets`); + }; + if (config.max == null) { + throw new Error(`"max" must be defined for range buckets`); + }; + if (config.min > config.max) { + throw new Error(`"min" must be less than "max"`); + } + config.step ??= 1; + }, + }, +}; + +function transformNumberFieldOptions(opts) { + opts.nullable = false; + opts.integer = true; +}; diff --git a/module/utils/databases/Memory.mjs b/module/utils/databases/Memory.mjs index 456a4c6..7ee598a 100644 --- a/module/utils/databases/Memory.mjs +++ b/module/utils/databases/Memory.mjs @@ -1,8 +1,9 @@ import { Database } from "./Database.mjs"; import { filterPrivateRows } from "../privacy.mjs"; import { Logger } from "../Logger.mjs"; +import { validateBucketConfig } from "../buckets.mjs"; -const { randomID, mergeObject } = foundry.utils; +const { deleteProperty, diffObject, expandObject, mergeObject, randomID } = foundry.utils; export class MemoryDatabase extends Database { static #tables = { @@ -10,6 +11,7 @@ export class MemoryDatabase extends Database { name: `Dice/d10`, buckets: { type: `range`, + locked: true, min: 1, max: 10, step: 1, @@ -23,6 +25,7 @@ export class MemoryDatabase extends Database { name: `Dice/d20`, buckets: { type: `range`, + locked: true, min: 1, max: 20, step: 1, @@ -36,6 +39,7 @@ export class MemoryDatabase extends Database { name: `Dice/d100`, buckets: { type: `range`, + locked: true, min: 1, max: 100, step: 1, @@ -45,11 +49,23 @@ export class MemoryDatabase extends Database { stacked: true, }, }, - "Count of Successes": { - name: `Count of Successes`, + "Successes Number": { + name: `Successes Number`, buckets: { type: `number`, min: 0, + }, + graph: { + type: `bar`, + stacked: true, + }, + }, + "Successes Range": { + name: `Successes Range`, + buckets: { + type: `range`, + min: 0, + max: 100, step: 1, }, graph: { @@ -61,9 +77,6 @@ export class MemoryDatabase extends Database { name: `Type of Result`, buckets: { type: `string`, - trim: true, // forced true - blank: false, // forced false - textSearch: false, // forced false choices: [`Normal`, `Popped Off`, `Downed`], }, graph: { @@ -81,6 +94,26 @@ export class MemoryDatabase extends Database { return true; }; + static getTableNames() { + const tables = new Set(); + for (const tableID of Object.keys(this.#tables)) { + const [ targetTable ] = tableID.split(`/`, 2); + tables.add(targetTable); + }; + return Array.from(tables); + }; + + static getSubtableNames(table) { + const subtables = new Set(); + for (const tableID of Object.keys(this.#tables)) { + const [ targetTable, targetSubtable ] = tableID.split(`/`, 2); + if (targetTable === table) { + subtables.add(targetSubtable); + } + }; + return Array.from(subtables); + }; + /** @returns {Array} */ static getTables() { return Object.values(this.#tables); @@ -90,6 +123,39 @@ export class MemoryDatabase extends Database { return this.#tables[tableID]; }; + static async updateTable(tableID, changes) { + Logger.debug({tableID, changes}); + const table = this.getTable(tableID); + if (!table) { return false }; + + // Bucket coercion in case called via the API + deleteProperty(changes, `name`); + deleteProperty(changes, `buckets.type`); + + const diff = diffObject( + table, + expandObject(changes), + { inner: true, deletionKeys: true }, + ); + if (Object.keys(diff).length === 0) { return false }; + + const updated = mergeObject( + table, + diff, + { inplace: false, performDeletions: true }, + ); + + try { + updated.buckets = validateBucketConfig(updated.buckets); + } catch (e) { + ui.notifications.error(e); + return false; + }; + + this.#tables[tableID] = updated; + return true; + }; + static createRow(table, userID, row, { rerender = true } = {}) { if (!this.#tables[table]) { return }; this.#rows[userID] ??= {}; diff --git a/module/utils/validateValue.mjs b/module/utils/validateValue.mjs deleted file mode 100644 index 3c51987..0000000 --- a/module/utils/validateValue.mjs +++ /dev/null @@ -1,73 +0,0 @@ -import { Logger } from "./Logger.mjs"; - -const { deepClone } = foundry.utils; -const { StringField, NumberField } = foundry.data.fields; - -/** - * @param {unknown} value The value to validate - * @param {BucketConfig} options The bucket config for the table - * @returns Whether or not the value is valid for the table - */ -export function validateValue(value, options) { - /** @type {BucketConfig} */ - let opts = deepClone(options); - if (validatorTypes[opts.type] == null) { - Logger.error(`Failed to find type validator for: ${opts.type}`); - return false; - }; - - const validator = validatorTypes[opts.type]; - validator.transformOptions(opts); - - const field = new validator.field(opts); - const error = field.validate(value); - - // DataFields return a class instance on error, or void when valid. - return !error; -}; - -export const BucketTypes = { - STRING: `string`, - NUMBER: `number`, - RANGE: `range`, -}; - -const validatorTypes = { - [BucketTypes.STRING]: { - field: StringField, - transformOptions: (opts) => { - delete opts.type; - opts.nullable = false; - opts.trim = true; - opts.blank = false; - if (typeof opts.choices === `function`) { - Logger.error(`Choices cannot be a function in a table's buckets configuraion`); - delete opts.choices; - }; - }, - }, - [BucketTypes.NUMBER]: { - field: NumberField, - transformOptions: (opts) => { - delete opts.type; - opts.nullable = false; - opts.integer = true; - if (typeof opts.choices === `function`) { - Logger.error(`Choices cannot be a function in a table's buckets configuraion`); - delete opts.choices; - }; - }, - }, - [BucketTypes.RANGE]: { - field: NumberField, - transformOptions: (opts) => { - delete opts.type; - opts.nullable = false; - opts.integer = true; - if (typeof opts.choices === `function`) { - Logger.error(`Choices cannot be a function in a table's buckets configuraion`); - delete opts.choices; - }; - }, - }, -}; diff --git a/public/styles/Apps/StatsViewer.css b/public/styles/Apps/StatsViewer.css index 8bdf452..5ef27e6 100644 --- a/public/styles/Apps/StatsViewer.css +++ b/public/styles/Apps/StatsViewer.css @@ -3,16 +3,6 @@ gap: 1rem; } - [data-application-part="tableSelect"] { - display: flex; - flex-direction: row; - gap: 1rem; - - > div { - width: 100%; - } - } - [data-application-part="dataFilters"] { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -25,10 +15,4 @@ justify-items: center; position: relative; } - - .control-group { - display: flex; - flex-direction: column; - gap: 2px; - } } diff --git a/public/styles/Apps/TableManager.css b/public/styles/Apps/TableManager.css new file mode 100644 index 0000000..e5eab2e --- /dev/null +++ b/public/styles/Apps/TableManager.css @@ -0,0 +1,63 @@ +.stat-tracker.TableManager { + min-width: 400px; + min-height: 200px; + max-height: initial; + + .window-content { + gap: 1rem; + } + + [data-application-part="buckets"] { + display: flex; + flex-direction: column; + gap: 0.5rem; + height: auto; + flex-grow: 1; + } + + .alert-box { + border-width: 2px; + border-style: dashed; + border-radius: 8px; + box-sizing: border-box; + padding: 1rem; + + &.warning { + background: color(from var(--color-level-warning-bg) srgb r g b / 0.1); + border-color: var(--color-level-warning); + } + + &.locked { + margin: -0.5rem; + padding: 0.5rem; + border-color: var(--color-level-error); + background: color(from var(--color-level-error-bg) srgb r g b / 0.25); + } + } + + .center { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + } + + .input-group { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 3fr); + align-items: center; + + .hint { + grid-column: 1 / -1; + } + } + + .flex-fill { + flex-grow: 1; + margin-bottom: -0.5rem; + } + + .save { + width: 100%; + } +} diff --git a/public/styles/Apps/common.css b/public/styles/Apps/common.css new file mode 100644 index 0000000..f2bd252 --- /dev/null +++ b/public/styles/Apps/common.css @@ -0,0 +1,21 @@ +.stat-tracker { + [data-application-part="tableSelect"] { + display: flex; + flex-direction: row; + gap: 1rem; + + > div { + width: 100%; + } + } + + .control-group { + display: flex; + flex-direction: column; + gap: 2px; + } + + .st-scrollable { + overflow: auto; + } +} diff --git a/public/styles/elements/span.css b/public/styles/elements/span.css new file mode 100644 index 0000000..36d3cc6 --- /dev/null +++ b/public/styles/elements/span.css @@ -0,0 +1,5 @@ +.stat-tracker span { + &.large { + font-size: var(--font-size-24); + } +} diff --git a/public/styles/elements/string-tags.css b/public/styles/elements/string-tags.css new file mode 100644 index 0000000..048852c --- /dev/null +++ b/public/styles/elements/string-tags.css @@ -0,0 +1,13 @@ +.stat-tracker string-tags { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px; + + > .tags { + grid-column: 1 / -1; + } + + > button { + height: 100%; + } +} diff --git a/public/styles/main.css b/public/styles/main.css index 4a4eea0..b235127 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -1,7 +1,12 @@ -@layer resets, elements, components, apps, exceptions; +@layer resets, elements, components, partials, apps, exceptions; @import url("./elements/custom-multi-select.css") layer(elements); +@import url("./elements/string-tags.css") layer(elements); +@import url("./elements/span.css") layer(elements); + +@import url("./Apps/common.css") layer(partials); @import url("./Apps/StatsViewer.css") layer(apps); @import url("./Apps/StatsSidebar.css") layer(apps); -@import url("./Apps/TableCreator.css") layer(apps); \ No newline at end of file +@import url("./Apps/TableCreator.css") layer(apps); +@import url("./Apps/TableManager.css") layer(apps); diff --git a/public/templates/Apps/TableManager/buckets/empty.hbs b/public/templates/Apps/TableManager/buckets/empty.hbs new file mode 100644 index 0000000..e4819fd --- /dev/null +++ b/public/templates/Apps/TableManager/buckets/empty.hbs @@ -0,0 +1,7 @@ +
+ {{#if table}} + Select a Subtable + {{else}} + Select a Table + {{/if}} +
diff --git a/public/templates/Apps/TableManager/buckets/number.hbs b/public/templates/Apps/TableManager/buckets/number.hbs new file mode 100644 index 0000000..455595d --- /dev/null +++ b/public/templates/Apps/TableManager/buckets/number.hbs @@ -0,0 +1,57 @@ +
+ {{#if buckets.locked}} +

+ This bucket configuration has been locked, preventing editing + of the settings. +

+ {{/if}} +
+ + +

+ The minimum allowed value. Leave empty for no minimum. +

+
+
+ + +

+ The maximum allowed value. Leave empty for no maximum. +

+
+
+ + +

+ The step value, if a minimum is not provided this will not be + saved and the existing value will be removed from the saved + configuration. +

+
+
diff --git a/public/templates/Apps/TableManager/buckets/range.hbs b/public/templates/Apps/TableManager/buckets/range.hbs new file mode 100644 index 0000000..e54d552 --- /dev/null +++ b/public/templates/Apps/TableManager/buckets/range.hbs @@ -0,0 +1,50 @@ +
+ {{#if buckets.locked}} +

+ This bucket configuration has been locked, preventing editing + of the settings. +

+ {{/if}} +
+ + +
+
+ + +
+
+ + +

+ The size of the step between values within the range. +

+
+
diff --git a/public/templates/Apps/TableManager/buckets/string.hbs b/public/templates/Apps/TableManager/buckets/string.hbs new file mode 100644 index 0000000..3f77240 --- /dev/null +++ b/public/templates/Apps/TableManager/buckets/string.hbs @@ -0,0 +1,17 @@ +
+
+ + +

+ These are the only values that are allowed to be provided to + the database and are case-sensitive. Leave this empty to + allow any string to be valid. Empty strings are never valid and + extra spaces are removed the start and end of the options. +

+
+
diff --git a/public/templates/Apps/TableManager/submit.hbs b/public/templates/Apps/TableManager/submit.hbs new file mode 100644 index 0000000..6e4dbf1 --- /dev/null +++ b/public/templates/Apps/TableManager/submit.hbs @@ -0,0 +1,5 @@ + diff --git a/public/templates/Apps/common/tableSelect.hbs b/public/templates/Apps/common/tableSelect.hbs index 5cb8192..785568d 100644 --- a/public/templates/Apps/common/tableSelect.hbs +++ b/public/templates/Apps/common/tableSelect.hbs @@ -7,6 +7,7 @@ id="{{meta.idp}}-selected-table" data-bind="_selectedTable" > + {{ st-options table tables }} @@ -19,6 +20,7 @@ id="{{meta.idp}}-selected-subtable" data-bind="_selectedSubtable" > + {{ st-options subtable subtables }}