Compare commits

...
2 Commits
6 changed files with 1900 additions and 314 deletions

No files matched your search

+81 -6
View File
@@ -16,6 +16,12 @@ export interface CharacterEntityRef {
entityRef: string;
}
export interface CharacterClassSelection {
class: CharacterEntityRef;
subclass: CharacterEntityRef | null;
level: number;
}
export interface CharacterProfile {
name: string;
level: number;
@@ -94,6 +100,7 @@ export interface CharacterSteps {
export interface CharacterSelections {
class: CharacterEntityRef | null;
subclass: CharacterEntityRef | null;
classes: CharacterClassSelection[];
background: CharacterEntityRef | null;
species: CharacterEntityRef | null;
}
@@ -219,6 +226,7 @@ const DEFAULT_STEPS: CharacterSteps = {
const DEFAULT_SELECTIONS: CharacterSelections = {
class: null,
subclass: null,
classes: [],
background: null,
species: null
};
@@ -375,6 +383,63 @@ function normalizeEntityRef(value: unknown, expectedType: CharacterEntityType):
};
}
function normalizeClassSelection(value: unknown): CharacterClassSelection | null {
if (!isRecord(value)) {
return null;
}
const classRef = normalizeEntityRef(value.class, "class");
if (!classRef) {
return null;
}
const subclassRef = normalizeEntityRef(value.subclass, "subclass");
return {
class: classRef,
subclass: subclassRef,
level: toInteger(value.level, 1, 1, 20)
};
}
function getTotalClassLevel(classes: CharacterClassSelection[]): number {
const total = classes.reduce((sum, cls) => sum + cls.level, 0);
return Math.max(1, Math.min(20, total));
}
function normalizeClassSelections(
value: unknown,
fallbackClass: CharacterEntityRef | null,
fallbackSubclass: CharacterEntityRef | null,
fallbackLevel: number
): CharacterClassSelection[] {
const out: CharacterClassSelection[] = [];
const seen = new Set<string>();
if (Array.isArray(value)) {
for (const raw of value) {
const normalized = normalizeClassSelection(raw);
if (!normalized) {
continue;
}
if (seen.has(normalized.class.entityRef)) {
continue;
}
seen.add(normalized.class.entityRef);
out.push(normalized);
}
}
if (!out.length && fallbackClass) {
out.push({
class: fallbackClass,
subclass: fallbackSubclass,
level: Math.max(1, Math.min(20, fallbackLevel))
});
}
return out;
}
function normalizeProfile(value: unknown): CharacterProfile {
const profile = isRecord(value) ? value : {};
return {
@@ -521,11 +586,17 @@ function normalizeAbout(value: unknown): CharacterAboutStep {
};
}
function normalizeSelections(value: unknown): CharacterSelections {
function normalizeSelections(value: unknown, fallbackLevel: number): CharacterSelections {
const selections = isRecord(value) ? value : {};
const primaryClass = normalizeEntityRef(selections.class, "class");
const primarySubclass = normalizeEntityRef(selections.subclass, "subclass");
const classes = normalizeClassSelections(selections.classes, primaryClass, primarySubclass, fallbackLevel);
const firstClass = classes[0];
return {
class: normalizeEntityRef(selections.class, "class"),
subclass: normalizeEntityRef(selections.subclass, "subclass"),
class: firstClass?.class ?? primaryClass,
subclass: firstClass?.subclass ?? primarySubclass,
classes,
background: normalizeEntityRef(selections.background, "background"),
species: normalizeEntityRef(selections.species, "species")
};
@@ -549,8 +620,12 @@ function normalizeCharacterRecord(raw: unknown): CharacterDocument {
const root = isRecord(raw) ? raw : {};
const profile = normalizeProfile(root.profile);
const selections = normalizeSelections(root.selections);
const selections = normalizeSelections(root.selections, profile.level);
const steps = normalizeSteps(root.steps);
const normalizedProfile: CharacterProfile = {
...profile,
level: selections.classes.length ? getTotalClassLevel(selections.classes) : profile.level
};
const idCandidate = toString(root.id).trim();
const id = idCandidate || randomUUID();
@@ -561,7 +636,7 @@ function normalizeCharacterRecord(raw: unknown): CharacterDocument {
id,
createdAt: toIsoDate(root.createdAt, nowIso),
updatedAt: toIsoDate(root.updatedAt, nowIso),
profile,
profile: normalizedProfile,
selections,
steps,
notes: toString(root.notes).trim()
@@ -655,7 +730,7 @@ export function listCharacters(charactersDir: string): CharacterSummary[] {
id: character.id,
name: character.profile.name,
level: character.profile.level,
className: character.selections.class?.name ?? null,
className: character.selections.classes[0]?.class.name ?? character.selections.class?.name ?? null,
backgroundName: character.selections.background?.name ?? null,
speciesName: character.selections.species?.name ?? null,
createdAt: character.createdAt,
+40 -7
View File
@@ -350,7 +350,7 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
addCharactersMenu();
addResetOnboardingMenu();
bindNativeNavbarSync();
postNativeNavbarModel();
queueNativeNavbarModelPost({ isForce: true });
};
const NAVBAR_ACTION_ATTR = "data-5etd-nav-action-id";
@@ -593,17 +593,50 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
return groups;
};
const postNativeNavbarModel = () => {
let navbarModelLastSignature = "";
let isNavbarModelPostQueued = false;
const postNativeNavbarModel = ({ isForce = false } = {}) => {
if (!window.parent || window.parent === window) return;
const groups = buildNativeNavbarGroups();
let nextSignature = "";
try {
nextSignature = JSON.stringify(groups);
} catch {
nextSignature = \`groups:\${groups.length}:\${Date.now()}\`;
}
if (!isForce && nextSignature === navbarModelLastSignature) {
return;
}
navbarModelLastSignature = nextSignature;
window.parent.postMessage(
{
type: "5etd:native-navbar-data",
groups: buildNativeNavbarGroups()
groups
},
"*"
);
};
const queueNativeNavbarModelPost = ({ isForce = false } = {}) => {
if (isForce) {
isNavbarModelPostQueued = false;
postNativeNavbarModel({ isForce: true });
return;
}
if (isNavbarModelPostQueued) {
return;
}
isNavbarModelPostQueued = true;
window.setTimeout(() => {
isNavbarModelPostQueued = false;
postNativeNavbarModel();
}, 80);
};
let navbarSyncObserver = null;
const bindNativeNavbarSync = () => {
const navbar = document.getElementById("navbar");
@@ -614,9 +647,9 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
navbar.dataset.desktopNavSyncBound = "1";
navbarSyncObserver?.disconnect?.();
navbarSyncObserver = new MutationObserver(() => {
window.requestAnimationFrame(postNativeNavbarModel);
queueNativeNavbarModelPost();
});
navbarSyncObserver.observe(navbar, { childList: true, subtree: true, attributes: true, characterData: true });
navbarSyncObserver.observe(navbar, { childList: true, subtree: true });
};
const runNativeNavbarAction = (actionId) => {
@@ -649,7 +682,7 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
if (data.type === "5etd:native-navbar-request") {
bindNativeNavbarSync();
postNativeNavbarModel();
queueNativeNavbarModelPost({ isForce: true });
return;
}
@@ -693,7 +726,7 @@ html.desktop-reskin .page__header {
border: 1px solid rgba(109, 164, 223, 0.32) !important;
border-radius: 18px;
background: linear-gradient(115deg, rgba(17, 29, 47, 0.98), rgba(24, 38, 58, 0.96) 64%, rgba(87, 58, 32, 0.88));
backdrop-filter: blur(14px);
// backdrop-filter: blur(14px);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.24);
}
+7
View File
@@ -76,6 +76,12 @@ interface CharacterEntityRef {
entityRef: string;
}
interface CharacterClassSelection {
class: CharacterEntityRef;
subclass: CharacterEntityRef | null;
level: number;
}
interface CharacterProfile {
name: string;
level: number;
@@ -151,6 +157,7 @@ interface CharacterDocument {
selections: {
class: CharacterEntityRef | null;
subclass: CharacterEntityRef | null;
classes: CharacterClassSelection[];
background: CharacterEntityRef | null;
species: CharacterEntityRef | null;
};
+1624 -288
View File
File diff suppressed because it is too large. Load diff
+141 -13
View File
@@ -69,7 +69,7 @@ body {
radial-gradient(circle at 16% 0%, rgba(71, 131, 206, 0.34), transparent 34%),
radial-gradient(circle at 88% -8%, rgba(234, 163, 78, 0.28), transparent 40%),
linear-gradient(180deg, rgba(14, 24, 40, 0.96), rgba(9, 16, 28, 0.97));
backdrop-filter: blur(12px);
/* backdrop-filter: blur(12px); */
position: relative;
overflow: visible;
isolation: isolate;
@@ -516,9 +516,9 @@ body {
border-radius: 16px;
background: linear-gradient(180deg, rgba(14, 22, 35, 0.98), rgba(8, 14, 24, 0.99));
box-shadow: var(--shadow);
display: grid;
grid-template-rows: auto auto 1fr;
overflow: hidden;
/* display: grid; */
/* grid-template-rows: auto auto 1fr; */
overflow: scroll;
}
.character-modal button,
@@ -1529,6 +1529,7 @@ body {
.entity-card-grid {
flex: 1;
min-height: 0;
max-height: clamp(260px, 44vh, 560px);
overflow: auto;
padding-right: 4px;
gap: 10px;
@@ -1737,10 +1738,18 @@ body {
color: #edf4ff;
overflow: hidden;
text-align: left;
padding: 0;
padding: 0 !important;
display: block;
min-height: 0;
aspect-ratio: 1 / 1;
cursor: pointer;
transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
}
.entity-tile:hover {
border-color: rgba(134, 183, 247, 0.88);
box-shadow: 0 0 0 1px rgba(121, 178, 249, 0.28);
transform: translateY(-1px);
}
.entity-tile-media {
@@ -1829,6 +1838,97 @@ body {
box-shadow: 0 0 0 1px rgba(252, 84, 84, 0.42);
}
.entity-tile.is-disabled {
opacity: 0.45;
filter: grayscale(0.5);
cursor: not-allowed;
}
.character-class-list {
border: 1px solid var(--line);
border-radius: 12px;
background: rgba(9, 14, 24, 0.72);
padding: 10px;
display: grid;
gap: 8px;
}
.character-class-list h4 {
margin: 0;
color: #eef4ff;
font-size: 0.96rem;
}
.character-class-list-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 8px;
}
.character-class-chip {
border: 1px solid var(--line);
border-radius: 10px;
background: rgba(7, 11, 20, 0.84);
padding: 8px;
display: grid;
gap: 8px;
}
.character-class-chip header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.character-class-chip header strong {
color: #eef4ff;
}
.character-class-chip header span {
font-size: 0.76rem;
color: var(--text-2);
}
.character-class-chip-badges {
display: inline-flex;
align-items: center;
gap: 6px;
}
.character-class-chip-role {
border: 1px solid rgba(73, 92, 120, 0.8);
border-radius: 999px;
padding: 1px 7px;
font-size: 0.68rem !important;
color: #c5d4ea !important;
background: rgba(19, 28, 42, 0.74);
}
.character-class-chip-role.is-primary {
border-color: rgba(51, 158, 102, 0.8);
color: #dcfff1 !important;
background: rgba(21, 108, 67, 0.64);
}
.character-class-chip-note {
margin: -2px 0 0;
font-size: 0.76rem;
color: var(--text-2);
}
.character-class-chip-actions {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.character-class-chip-actions span {
font-size: 0.86rem;
color: var(--text-1);
}
.entity-popover-backdrop {
position: fixed;
inset: 0;
@@ -1837,17 +1937,18 @@ body {
place-items: center;
padding: 18px;
background: rgba(2, 6, 12, 0.74);
backdrop-filter: blur(2px);
/* backdrop-filter: blur(2px); */
}
.entity-popover {
width: 70%;
max-height: calc(70vh - 36px);
overflow: auto;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid var(--line-strong);
border-radius: 14px;
background: rgba(8, 12, 20, 0.98);
padding: 12px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
}
@@ -1856,9 +1957,8 @@ body {
align-items: center;
justify-content: space-between;
gap: 10px;
padding-bottom: 10px;
padding: 12px;
border-bottom: 1px solid var(--line);
margin-bottom: 10px;
}
.entity-popover-header h4 {
@@ -1867,6 +1967,22 @@ body {
font-size: 1.05rem;
}
.entity-popover-body {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
padding: 12px;
}
.entity-popover-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px;
border-top: 1px solid var(--line);
background: rgba(5, 9, 16, 0.94);
}
.entity-detail--popover {
min-height: 0;
}
@@ -1886,6 +2002,7 @@ body {
.chooser-layout {
min-height: 0;
max-height: 62vh;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 12px;
@@ -2083,10 +2200,12 @@ body {
.spell-level-group {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.spell-level-group h5 {
grid-column: 1 / -1;
margin: 0;
color: #f2f5ff;
font-size: 0.9rem;
@@ -2109,6 +2228,10 @@ body {
background: linear-gradient(145deg, rgba(121, 39, 39, 0.64), rgba(25, 22, 33, 0.95));
}
.spell-pick-card.is-disabled {
opacity: 0.7;
}
.spell-pick-title {
font-size: 0.9rem;
font-weight: 700;
@@ -2249,10 +2372,10 @@ body {
width: 100%;
}
.ability-score-strip,
/* .ability-score-strip,
.ability-assignment-grid--ability-step {
grid-template-columns: 1fr;
}
} */
.chooser-layout {
grid-template-columns: 1fr;
@@ -2271,6 +2394,12 @@ body {
}
}
@media (width <= 900px) {
.spell-level-group {
grid-template-columns: 1fr;
}
}
@media (width <= 900px) {
.entity-list-actions {
justify-content: stretch;
@@ -2305,7 +2434,6 @@ body {
width: calc(100vw - 20px);
max-height: calc(100vh - 20px);
border-radius: 12px;
padding: 10px;
}
.entity-popover-header {
+7
View File
@@ -92,6 +92,12 @@ interface CharacterEntityRef {
entityRef: string;
}
interface CharacterClassSelection {
class: CharacterEntityRef;
subclass: CharacterEntityRef | null;
level: number;
}
interface CharacterProfile {
name: string;
level: number;
@@ -167,6 +173,7 @@ interface CharacterDocument {
selections: {
class: CharacterEntityRef | null;
subclass: CharacterEntityRef | null;
classes: CharacterClassSelection[];
background: CharacterEntityRef | null;
species: CharacterEntityRef | null;
};