initial commit
This commit is contained in:
commit
af8b02071e
6 changed files with 715 additions and 0 deletions
32
README.md
Normal file
32
README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Daggerheart Custom Attribution Sources
|
||||||
|
|
||||||
|
A Foundry VTT (Version 14) module for the **Daggerheart** system that allows Game Masters to add, update, and remove custom item attribution sources directly from the module settings.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **No Overwrites**: Safely registers custom sources to the system `CONFIG.DH.GENERAL.attributionSources` configuration without altering or affecting system defaults or attributions from other modules.
|
||||||
|
- **Modern UI/UX**: Built using Foundry VTT v14's modern `ApplicationV2` interface with a dark layout matching Daggerheart's crimson and gold design aesthetics.
|
||||||
|
- **Dynamic Configuration**: Add source groups (e.g. `Forevermore`) and nest multiple values inside them (e.g. `Campaign - Forevermore`).
|
||||||
|
- **Autocompleter Integration**: Custom attributions automatically populate autocomplete lists when editing any item's attribution details.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
dh-attribution-sources/
|
||||||
|
├── module.json # Module registration and compatibility manifest
|
||||||
|
├── scripts/
|
||||||
|
│ └── dh-attribution-sources.mjs # Module entrypoint & ApplicationV2 form logic
|
||||||
|
├── styles/
|
||||||
|
│ └── dh-attribution-sources.css # Crimson & Gold CSS styles for the settings dialog
|
||||||
|
└── templates/
|
||||||
|
├── footer.hbs # Save and Reset buttons template
|
||||||
|
└── settings.hbs # Layout template for editing groups and nested values
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Install & Use
|
||||||
|
|
||||||
|
1. Ensure the `dh-attribution-sources` directory is placed inside your Foundry VTT `Data/modules` folder.
|
||||||
|
2. Launch Foundry VTT, open your game world, go to the **Manage Modules** tab in the sidebar, and check **Daggerheart Custom Attribution Sources**.
|
||||||
|
3. In the sidebar under **Configure Game Settings** -> **Daggerheart Custom Attribution Sources**, click the **Manage Custom Sources** menu.
|
||||||
|
4. Add source groups and values, configure their labels, and click **Save Settings**.
|
||||||
|
5. Edit any item sheet, click the **Attribution** icon in the header, and begin typing in the source field to see your custom sources appear as autocomplete recommendations.
|
||||||
29
module.json
Normal file
29
module.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"id": "dh-attribution-sources",
|
||||||
|
"title": "Daggerheart Custom Attribution Sources",
|
||||||
|
"description": "Allows GMs to add, update, and remove custom attribution sources for items without modifying default system or other modules' attribution sources.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"compatibility": {
|
||||||
|
"minimum": "12",
|
||||||
|
"verified": "14"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "cptn_cosmo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relationships": {
|
||||||
|
"systems": [
|
||||||
|
{
|
||||||
|
"id": "daggerheart",
|
||||||
|
"type": "system"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"esmodules": [
|
||||||
|
"scripts/dh-attribution-sources.mjs"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"styles/dh-attribution-sources.css"
|
||||||
|
]
|
||||||
|
}
|
||||||
233
scripts/dh-attribution-sources.mjs
Normal file
233
scripts/dh-attribution-sources.mjs
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
let registeredKeys = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the custom attribution sources to the system CONFIG object.
|
||||||
|
*/
|
||||||
|
export function applyAttributionSources() {
|
||||||
|
if (!CONFIG.DH?.GENERAL?.attributionSources) return;
|
||||||
|
|
||||||
|
// Remove previously registered custom sources
|
||||||
|
for (const key of registeredKeys) {
|
||||||
|
delete CONFIG.DH.GENERAL.attributionSources[key];
|
||||||
|
}
|
||||||
|
registeredKeys.clear();
|
||||||
|
|
||||||
|
// Get current custom sources from setting
|
||||||
|
const customSources = game.settings.get("dh-attribution-sources", "customSources") || [];
|
||||||
|
|
||||||
|
// Inject custom sources
|
||||||
|
for (const source of customSources) {
|
||||||
|
if (!source.id) continue;
|
||||||
|
const key = `custom_${source.id}`;
|
||||||
|
|
||||||
|
CONFIG.DH.GENERAL.attributionSources[key] = {
|
||||||
|
label: source.label || source.id,
|
||||||
|
values: (source.values || []).map(val => ({
|
||||||
|
label: val.label || "",
|
||||||
|
hint: val.hint || ""
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
registeredKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom application sheet to add, update, and remove custom attribution sources.
|
||||||
|
*/
|
||||||
|
export class CustomAttributionSourcesForm extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
constructor(options = {}) {
|
||||||
|
super(options);
|
||||||
|
const saved = game.settings.get("dh-attribution-sources", "customSources") || [];
|
||||||
|
this.sources = foundry.utils.deepClone(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
tag: 'form',
|
||||||
|
id: 'dh-attribution-sources-settings',
|
||||||
|
classes: ['dh-attribution-sources', 'dialog', 'settings-app'],
|
||||||
|
position: { width: 620, height: 650 },
|
||||||
|
window: {
|
||||||
|
title: "Custom Attribution Sources",
|
||||||
|
icon: 'fa-solid fa-signature',
|
||||||
|
resizable: true
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addSource: CustomAttributionSourcesForm.#onAddSource,
|
||||||
|
removeSource: CustomAttributionSourcesForm.#onRemoveSource,
|
||||||
|
addValue: CustomAttributionSourcesForm.#onAddValue,
|
||||||
|
removeValue: CustomAttributionSourcesForm.#onRemoveValue,
|
||||||
|
reset: CustomAttributionSourcesForm.#onReset,
|
||||||
|
save: CustomAttributionSourcesForm.#onSave
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
handler: CustomAttributionSourcesForm.#onSubmit,
|
||||||
|
submitOnChange: false,
|
||||||
|
closeOnSubmit: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: {
|
||||||
|
template: 'modules/dh-attribution-sources/templates/settings.hbs',
|
||||||
|
scrollable: ['.sources-list']
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
template: 'modules/dh-attribution-sources/templates/footer.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareContext(options) {
|
||||||
|
const context = await super._prepareContext(options);
|
||||||
|
context.sources = this.sources;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
static #serializeForm(target) {
|
||||||
|
const form = target.closest('form');
|
||||||
|
const formData = new foundry.applications.ux.FormDataExtended(form);
|
||||||
|
const data = foundry.utils.expandObject(formData.object);
|
||||||
|
|
||||||
|
// Convert dot-notation structures back to nested arrays
|
||||||
|
const sources = Object.values(data.sources || {}).map(src => {
|
||||||
|
const values = Object.values(src.values || {}).map(val => ({
|
||||||
|
label: val.label || "",
|
||||||
|
hint: val.hint || ""
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
id: src.id || "",
|
||||||
|
label: src.label || "",
|
||||||
|
values: values
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onAddSource(event, target) {
|
||||||
|
const sources = CustomAttributionSourcesForm.#serializeForm(target);
|
||||||
|
sources.push({
|
||||||
|
id: `group-${foundry.utils.randomID(4)}`,
|
||||||
|
label: "New Source Group",
|
||||||
|
values: [{ label: "", hint: "" }]
|
||||||
|
});
|
||||||
|
this.sources = sources;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onRemoveSource(event, target) {
|
||||||
|
const sources = CustomAttributionSourcesForm.#serializeForm(target);
|
||||||
|
const index = parseInt(target.dataset.sourceIndex);
|
||||||
|
if (!isNaN(index)) {
|
||||||
|
sources.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.sources = sources;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onAddValue(event, target) {
|
||||||
|
const sources = CustomAttributionSourcesForm.#serializeForm(target);
|
||||||
|
const index = parseInt(target.dataset.sourceIndex);
|
||||||
|
if (!isNaN(index)) {
|
||||||
|
sources[index].values.push({ label: "", hint: "" });
|
||||||
|
}
|
||||||
|
this.sources = sources;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onRemoveValue(event, target) {
|
||||||
|
const sources = CustomAttributionSourcesForm.#serializeForm(target);
|
||||||
|
const sourceIndex = parseInt(target.dataset.sourceIndex);
|
||||||
|
const valueIndex = parseInt(target.dataset.valueIndex);
|
||||||
|
if (!isNaN(sourceIndex) && !isNaN(valueIndex)) {
|
||||||
|
sources[sourceIndex].values.splice(valueIndex, 1);
|
||||||
|
}
|
||||||
|
this.sources = sources;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onReset(event, target) {
|
||||||
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
window: {
|
||||||
|
title: "Reset Custom Attribution Sources"
|
||||||
|
},
|
||||||
|
content: "Are you sure you want to discard your unsaved changes and reload settings?"
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
const saved = game.settings.get("dh-attribution-sources", "customSources") || [];
|
||||||
|
this.sources = foundry.utils.deepClone(saved);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onSave(event, target) {
|
||||||
|
const sources = CustomAttributionSourcesForm.#serializeForm(target);
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
const ids = new Set();
|
||||||
|
for (const src of sources) {
|
||||||
|
const cleanId = src.id.trim();
|
||||||
|
if (!cleanId) {
|
||||||
|
ui.notifications.error("All attribution source groups must have a non-empty ID.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ids.has(cleanId)) {
|
||||||
|
ui.notifications.error(`Duplicate ID found: "${cleanId}". Each attribution source group must have a unique ID.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ids.add(cleanId);
|
||||||
|
|
||||||
|
src.id = cleanId;
|
||||||
|
src.label = src.label.trim() || cleanId;
|
||||||
|
|
||||||
|
if (src.values.length === 0) {
|
||||||
|
ui.notifications.error(`Group "${src.label}" must contain at least one attribution value.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const val of src.values) {
|
||||||
|
val.label = val.label.trim();
|
||||||
|
val.hint = val.hint.trim();
|
||||||
|
if (!val.label) {
|
||||||
|
ui.notifications.error(`Group "${src.label}" has an empty attribution label.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.settings.set("dh-attribution-sources", "customSources", sources);
|
||||||
|
applyAttributionSources();
|
||||||
|
ui.notifications.info("Custom attribution sources saved successfully.");
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onSubmit(event, form, formData) {
|
||||||
|
// Form submit falls through; handled by onSave button click
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook into initialization to register the settings and configuration panel
|
||||||
|
Hooks.once('init', () => {
|
||||||
|
game.settings.register("dh-attribution-sources", "customSources", {
|
||||||
|
name: "Custom Attribution Sources List",
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: Array,
|
||||||
|
default: []
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.registerMenu("dh-attribution-sources", "manageSources", {
|
||||||
|
name: "Custom Attribution Sources",
|
||||||
|
label: "Manage Custom Sources",
|
||||||
|
hint: "Configure your custom item attribution sources.",
|
||||||
|
icon: "fas fa-signature",
|
||||||
|
type: CustomAttributionSourcesForm,
|
||||||
|
restricted: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup hook to run once the system has registered its configuration
|
||||||
|
Hooks.on('setup', () => {
|
||||||
|
applyAttributionSources();
|
||||||
|
});
|
||||||
347
styles/dh-attribution-sources.css
Normal file
347
styles/dh-attribution-sources.css
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
/* Main application window styling */
|
||||||
|
.dh-attribution-sources.settings-app {
|
||||||
|
--dh-font-family: 'Outfit', 'Signika', 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
--dh-color-bg-dark: #121214;
|
||||||
|
--dh-color-bg-card: rgba(30, 30, 35, 0.65);
|
||||||
|
--dh-color-bg-input: rgba(10, 10, 12, 0.7);
|
||||||
|
--dh-color-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--dh-color-border-hover: rgba(197, 160, 89, 0.4);
|
||||||
|
--dh-color-primary: #9e1b1b;
|
||||||
|
--dh-color-primary-hover: #c02222;
|
||||||
|
--dh-color-gold: #c5a059;
|
||||||
|
--dh-color-gold-hover: #dfb96c;
|
||||||
|
--dh-color-text-bright: #f0edf4;
|
||||||
|
--dh-color-text-muted: #a6a3ad;
|
||||||
|
--dh-color-error: #e74c3c;
|
||||||
|
--dh-color-error-hover: #ff6b6b;
|
||||||
|
--dh-color-success: #2ecc71;
|
||||||
|
--dh-color-success-hover: #2ee07e;
|
||||||
|
|
||||||
|
font-family: var(--dh-font-family);
|
||||||
|
background: var(--dh-color-bg-dark) !important;
|
||||||
|
border: 1px solid rgba(197, 160, 89, 0.25) !important;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
.dh-attribution-sources-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--dh-color-text-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--dh-color-text-muted);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--dh-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollable source list */
|
||||||
|
.dh-attribution-sources-container .sources-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Source Group Card */
|
||||||
|
.dh-attribution-sources-container .source-group-card {
|
||||||
|
background: var(--dh-color-bg-card);
|
||||||
|
border: 1px solid var(--dh-color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
position: relative;
|
||||||
|
transition: border-color 0.25s ease, box-shadow 0.25s ease;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .source-group-card:hover {
|
||||||
|
border-color: var(--dh-color-border-hover);
|
||||||
|
box-shadow: 0 4px 15px rgba(197, 160, 89, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Group Header layout */
|
||||||
|
.dh-attribution-sources-container .group-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px dashed var(--dh-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .group-title-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .group-title-row .form-group {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container label.compact-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--dh-color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom inputs */
|
||||||
|
.dh-attribution-sources-container input[type="text"] {
|
||||||
|
background: var(--dh-color-bg-input) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||||
|
color: var(--dh-color-text-bright) !important;
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
width: 100%;
|
||||||
|
height: 32px !important;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
font-family: var(--dh-font-family);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container input[type="text"]:focus {
|
||||||
|
border-color: var(--dh-color-gold) !important;
|
||||||
|
box-shadow: 0 0 6px rgba(197, 160, 89, 0.4) !important;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete group button styling */
|
||||||
|
.dh-attribution-sources-container .remove-group-btn {
|
||||||
|
background: transparent !important;
|
||||||
|
border: 1px solid rgba(231, 76, 60, 0.3) !important;
|
||||||
|
color: var(--dh-color-error) !important;
|
||||||
|
padding: 8px 10px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
width: 36px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .remove-group-btn:hover {
|
||||||
|
background: rgba(231, 76, 60, 0.15) !important;
|
||||||
|
border-color: var(--dh-color-error-hover) !important;
|
||||||
|
color: var(--dh-color-error-hover) !important;
|
||||||
|
box-shadow: 0 0 8px rgba(231, 76, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Values section */
|
||||||
|
.dh-attribution-sources-container .values-section {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .values-section h6 {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--dh-color-text-bright);
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
border-left: 2px solid var(--dh-color-gold);
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .values-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Single value row styling */
|
||||||
|
.dh-attribution-sources-container .value-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .value-row .form-group.val-input {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove single value button styling */
|
||||||
|
.dh-attribution-sources-container .remove-value-btn {
|
||||||
|
background: transparent !important;
|
||||||
|
border: 1px solid rgba(231, 76, 60, 0.2) !important;
|
||||||
|
color: var(--dh-color-error) !important;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .remove-value-btn:hover {
|
||||||
|
background: rgba(231, 76, 60, 0.1) !important;
|
||||||
|
border-color: var(--dh-color-error-hover) !important;
|
||||||
|
color: var(--dh-color-error-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.dh-attribution-sources-container .add-value-btn {
|
||||||
|
background: rgba(197, 160, 89, 0.1) !important;
|
||||||
|
border: 1px dashed var(--dh-color-gold) !important;
|
||||||
|
color: var(--dh-color-gold) !important;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 5px 12px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .add-value-btn:hover {
|
||||||
|
background: rgba(197, 160, 89, 0.2) !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
color: var(--dh-color-text-bright) !important;
|
||||||
|
box-shadow: 0 0 6px rgba(197, 160, 89, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .add-group-btn {
|
||||||
|
background: var(--dh-color-primary) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: white !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .add-group-btn:hover {
|
||||||
|
background: var(--dh-color-primary-hover) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 12px rgba(158, 27, 27, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .form-actions-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--dh-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.dh-attribution-sources-container .no-sources-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--dh-color-bg-card);
|
||||||
|
border: 1px dashed var(--dh-color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--dh-color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .no-sources-message i {
|
||||||
|
color: var(--dh-color-gold);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Footer styling */
|
||||||
|
#dh-attribution-sources-settings footer.form-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(15, 15, 18, 0.95);
|
||||||
|
border-top: 1px solid rgba(197, 160, 89, 0.25);
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dh-attribution-sources-settings footer.form-footer button {
|
||||||
|
flex: 1;
|
||||||
|
font-family: var(--dh-font-family);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dh-attribution-sources-settings footer.form-footer button[data-action="reset"] {
|
||||||
|
background: transparent !important;
|
||||||
|
border: 1px solid var(--dh-color-border) !important;
|
||||||
|
color: var(--dh-color-text-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dh-attribution-sources-settings footer.form-footer button[data-action="reset"]:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
color: var(--dh-color-text-bright) !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dh-attribution-sources-settings footer.form-footer button.save-btn {
|
||||||
|
background: var(--dh-color-gold) !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15) !important;
|
||||||
|
color: #121214 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dh-attribution-sources-settings footer.form-footer button.save-btn:hover {
|
||||||
|
background: var(--dh-color-gold-hover) !important;
|
||||||
|
box-shadow: 0 0 10px rgba(197, 160, 89, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar for modern aesthetics */
|
||||||
|
.dh-attribution-sources-container .sources-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .sources-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .sources-list::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-attribution-sources-container .sources-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--dh-color-gold);
|
||||||
|
}
|
||||||
8
templates/footer.hbs
Normal file
8
templates/footer.hbs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<footer class="form-footer flexrow">
|
||||||
|
<button type="button" data-action="reset">
|
||||||
|
<i class="fa-solid fa-arrow-rotate-left"></i> Reset Changes
|
||||||
|
</button>
|
||||||
|
<button type="button" data-action="save" class="save-btn">
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i> Save Settings
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
66
templates/settings.hbs
Normal file
66
templates/settings.hbs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<div class="dh-attribution-sources-container">
|
||||||
|
<p class="description">
|
||||||
|
Manage custom attribution sources. These groups and values will populate the autocomplete suggestions when editing an item's attribution.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="sources-list">
|
||||||
|
{{#each sources as |source sourceIndex|}}
|
||||||
|
<div class="source-group-card" data-source-index="{{sourceIndex}}">
|
||||||
|
<div class="group-header">
|
||||||
|
<div class="group-title-row">
|
||||||
|
<div class="form-group flexrow">
|
||||||
|
<label class="compact-label">ID (slug)</label>
|
||||||
|
<input type="text" name="sources.{{sourceIndex}}.id" value="{{source.id}}" placeholder="e.g., campaign-srd" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group flexrow">
|
||||||
|
<label class="compact-label">Label (name)</label>
|
||||||
|
<input type="text" name="sources.{{sourceIndex}}.label" value="{{source.label}}" placeholder="e.g., Campaign SRD" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="remove-group-btn" data-action="removeSource" data-source-index="{{sourceIndex}}" data-tooltip="Remove Source Group">
|
||||||
|
<i class="fas fa-trash-can"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="values-section">
|
||||||
|
<header class="section-header flexrow">
|
||||||
|
<h6>Attribution Values</h6>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="values-list">
|
||||||
|
{{#each source.values as |val valIndex|}}
|
||||||
|
<div class="value-row flexrow">
|
||||||
|
<div class="form-group val-input flexrow">
|
||||||
|
<input type="text" name="sources.{{sourceIndex}}.values.{{valIndex}}.label" value="{{val.label}}" placeholder="Label (e.g., Campaign - Forevermore)" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group val-input flexrow">
|
||||||
|
<input type="text" name="sources.{{sourceIndex}}.values.{{valIndex}}.hint" value="{{val.hint}}" placeholder="Hint / Tooltip (optional)" />
|
||||||
|
</div>
|
||||||
|
<button type="button" class="remove-value-btn" data-action="removeValue" data-source-index="{{sourceIndex}}" data-value-index="{{valIndex}}" data-tooltip="Remove Value">
|
||||||
|
<i class="fas fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-actions flexrow">
|
||||||
|
<button type="button" class="add-value-btn" data-action="addValue" data-source-index="{{sourceIndex}}">
|
||||||
|
<i class="fas fa-plus"></i> Add Value
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="no-sources-message">
|
||||||
|
<p><i class="fa-solid fa-signature fa-2x"></i></p>
|
||||||
|
<p>No custom attribution sources configured. Click below to add your first one!</p>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions-bar">
|
||||||
|
<button type="button" class="add-group-btn" data-action="addSource">
|
||||||
|
<i class="fas fa-plus-circle"></i> Add Custom Source Group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue