Give NPCs their own editor section and unify the world-state boxes
NPCs are still part of stat_schema, but editing them inside the World State panel meant a nested dashed sub-box among four flat stat lists. They now get a top-level "Cast & NPCs" section: one card per NPC (avatar, name, npc.<id> address, triggers, description, its own stats) in a responsive grid, saved through the same schema path via the new NpcEditor/addNpc exports. The panel's real inconsistency was CSS, not layout: the global field rule keys off input[type="..."], which never matches the editor's typeless key and description inputs, so those rendered with browser defaults beside properly styled number boxes. One scoped base rule now covers the whole editor and every entry is the same tile with a caption over each control. Two bugs fell out of that: .se-band-n (0,1,0) always lost to input[type="number"] (0,1,1), so band bounds rendered full-width; and the exclusion has to be :not(:where([type="checkbox"])) because a plain :not() inherits its argument's specificity and swallows every override. In Play, NPCs group under a single "Cast" heading as compact plates instead of each becoming another top-level group in the rail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UeQVy5bEjLhfgWNc27Efet
This commit is contained in:
co-authored by
Claude Opus 5
parent
d66fd1d6a2
commit
be6dbb0f24
+165
-30
@@ -641,6 +641,37 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
|
||||
background: var(--accent);
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
/* Cast — each NPC is a small plate inside the one "Cast" group, so a roster of
|
||||
five doesn't read as five more unrelated sections in the rail. */
|
||||
.ws-npc {
|
||||
padding: 7px 9px;
|
||||
margin-bottom: 7px;
|
||||
border: 1px solid var(--border);
|
||||
border-left: 2px solid var(--accent-dim);
|
||||
border-radius: 7px;
|
||||
background: rgba(255, 255, 255, 0.022);
|
||||
}
|
||||
.ws-npc:last-child { margin-bottom: 0; }
|
||||
.ws-npc-head { display: flex; align-items: center; gap: 7px; margin-bottom: 3px; }
|
||||
.ws-npc-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 22px; height: 22px;
|
||||
border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.6rem;
|
||||
color: #14100a;
|
||||
background: linear-gradient(150deg, var(--accent-bright), var(--accent-dim));
|
||||
}
|
||||
.ws-npc-name {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ws-none { font-size: 0.78rem; color: var(--text-dim); font-style: italic; }
|
||||
.ws-milestones { list-style: none; margin: 0; padding: 0; }
|
||||
.ws-milestones li {
|
||||
display: flex;
|
||||
@@ -749,35 +780,47 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
|
||||
.schema-desc { color: var(--text-dim); font-size: 0.8rem; margin-top: 2px; }
|
||||
.schema-bands { color: var(--text-dim); font-size: 0.76rem; margin-top: 2px; font-style: italic; }
|
||||
|
||||
/* ---- Form-based schema editor ---- */
|
||||
/* ---- Form-based schema editor ----
|
||||
One shape for everything: a section is a panel, every entry inside it is a
|
||||
`.se-row` tile, and every control wears a `.se-field` caption. Adding a new
|
||||
kind of entry means reusing those three, not inventing a fourth box. */
|
||||
.se-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
padding: 13px 15px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.se-section-nested {
|
||||
margin: 8px 0 0;
|
||||
background: transparent;
|
||||
border-style: dashed;
|
||||
margin: 10px 0 0;
|
||||
padding: 11px 12px;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.se-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.se-section-title {
|
||||
font-size: 0.74rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--accent);
|
||||
}
|
||||
.se-hint {
|
||||
margin: -2px 0 10px;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.se-empty { color: var(--text-dim); font-size: 0.82rem; padding: 4px 0; }
|
||||
.se-add, .se-add-sm {
|
||||
font-size: 0.78rem;
|
||||
padding: 3px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.se-add-sm { margin-top: 6px; }
|
||||
.se-remove {
|
||||
@@ -788,53 +831,145 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
|
||||
padding: 2px 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.se-remove:hover { color: var(--danger, #e5484d); }
|
||||
.se-stat, .se-npc {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
/* Every control in the editor, one look. The global field rule keys off
|
||||
`input[type="…"]`, so the plain `<input>`s here (keys, descriptions) never
|
||||
matched it and fell back to browser defaults — that mismatch is most of why
|
||||
this panel used to read as a pile of unrelated boxes. Everything below is
|
||||
qualified with `.se` so it outranks that global rule (0,1,1) too. */
|
||||
/* `:where()` so the exclusion costs no specificity — a plain
|
||||
`:not([type="checkbox"])` would rank this above the `.se .se-*` overrides
|
||||
below and swallow every one of them. */
|
||||
.se input:not(:where([type="checkbox"])), .se textarea, .se select {
|
||||
width: 100%;
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
font-size: 0.84rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.se-stat:last-child, .se-npc:last-child { border-bottom: none; }
|
||||
.se-npc {
|
||||
|
||||
/* One entry — a stat, a flag, a milestone. */
|
||||
.se-row {
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.018);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.se-row-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.se-key {
|
||||
.se-row:last-child { margin-bottom: 0; }
|
||||
.se-row:focus-within { border-color: var(--border-bright); }
|
||||
/* Wraps because a phone-width row can't fit key + toggle + ✕ side by side, and
|
||||
an input's default `min-width: auto` would push the ✕ off-screen instead. */
|
||||
.se-row-top { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.se .se-key {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.85rem;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.82rem;
|
||||
min-width: 0;
|
||||
max-width: 220px;
|
||||
}
|
||||
/* Only in the row context — `.se-field` is a *column* flex, where a basis of
|
||||
140px would set the input's height instead of its width. */
|
||||
.se .se-row-top .se-key { flex: 1 1 140px; }
|
||||
.se-field { display: flex; flex-direction: column; gap: 3px; }
|
||||
.se-field > span {
|
||||
font-size: 0.66rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.09em;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.se-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.se-num { display: flex; flex-direction: column; gap: 2px; }
|
||||
.se-num span { font-size: 0.7rem; color: var(--text-dim); }
|
||||
.se-num input, .se-num select { width: 72px; padding: 3px 6px; font-size: 0.82rem; border-radius: 6px; }
|
||||
.se-kind select { width: 118px; }
|
||||
.se-initial-text input { width: 220px; }
|
||||
.se .se-num input, .se .se-num select { width: 72px; padding: 4px 6px; }
|
||||
.se .se-kind select { width: 132px; }
|
||||
.se .se-initial-text input { width: 220px; max-width: 100%; }
|
||||
.se-check {
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
font-size: 0.8rem; color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.se-check input { width: auto; }
|
||||
.se-text {
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
/* The global textarea rule also forces min-height:70px, which a 2-row inline
|
||||
field doesn't want. */
|
||||
.se .se-area { min-height: 0; line-height: 1.45; resize: vertical; }
|
||||
.se-sub-head { font-size: 0.7rem; color: var(--text-dim); margin: 4px 0 4px; }
|
||||
.se-band { display: flex; gap: 6px; align-items: center; margin-bottom: 4px; }
|
||||
.se-band-n { width: 64px; padding: 3px 6px; font-size: 0.82rem; }
|
||||
.se-band-l { flex: 1; padding: 3px 6px; font-size: 0.82rem; }
|
||||
/* Qualified so it outweighs the global input[type="number"] rule (0,1,1) —
|
||||
as a bare class it lost, and the band bounds rendered full-width. */
|
||||
.se .se-band-n { width: 64px; padding: 4px 6px; }
|
||||
.se .se-band-l { flex: 1 1 60px; width: auto; min-width: 0; }
|
||||
.se-bands { margin-top: 8px; }
|
||||
|
||||
/* ---- NPC roster (its own page section) ---- */
|
||||
.se-npc-grid {
|
||||
display: grid;
|
||||
/* min() so a 320px phone gets one column instead of a 340px overflow */
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
.se-npc {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-panel);
|
||||
overflow: hidden;
|
||||
}
|
||||
.se-npc:focus-within { border-color: var(--border-bright); }
|
||||
.se-npc-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: linear-gradient(180deg, rgba(212, 169, 78, 0.07), transparent);
|
||||
}
|
||||
.se-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: #14100a;
|
||||
background: linear-gradient(150deg, var(--accent-bright), var(--accent-dim));
|
||||
}
|
||||
.se-npc-ident { flex: 1; min-width: 0; }
|
||||
.se .se-npc-name {
|
||||
padding: 2px 6px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.95rem;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
.se .se-npc-name:focus { background: var(--bg-input); border-color: var(--border-bright); }
|
||||
.se-npc-addr {
|
||||
display: block;
|
||||
margin: 1px 0 0 6px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.se-npc-count {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.se-npc-body { padding: 12px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.se-npc-idfield .se-key { max-width: 100%; }
|
||||
.ws-report { list-style: none; margin: 4px 0 0; padding: 0; font-size: 0.82rem; }
|
||||
.ws-report li { padding: 2px 0; color: var(--text); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user