this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

base power tree layout

popup needs work

+330 -226
+247
src/components/astral/Power.astro
··· 1 + --- 2 + import { parentPowers, powers } from "../../consts/astral"; 3 + import { 4 + powerData, 5 + type Power, 6 + type powerId, 7 + type powerTree, 8 + } from "../../consts/astral"; 9 + 10 + interface Props { 11 + name: string; 12 + forceSelect?: boolean | undefined; 13 + description: string; 14 + image: { 15 + src: string; 16 + alt: string; 17 + }; 18 + meta: { 19 + name: string; 20 + value: string; 21 + }[]; 22 + id: powerId; 23 + children: (typeof powerTree)[]; 24 + first?: true; 25 + } 26 + 27 + const { name, description, image, meta, id, children, first } = Astro.props; 28 + const findDeepChildren = (children: (typeof powerTree)[]): number => { 29 + return children.reduce( 30 + (acc, curr) => 31 + acc + (curr.children.length > 0 ? findDeepChildren(curr.children) : 1), 32 + 0 33 + ); 34 + }; 35 + const deepChildren = findDeepChildren(children); 36 + 37 + /** find depth; assumes all branches are of equal depth */ 38 + const findDepth = (children: (typeof powerTree)[]): number => { 39 + return children.length === 0 ? 1 : 1 + findDepth(children[0].children); 40 + }; 41 + const depth = findDepth(children); 42 + --- 43 + 44 + <section 45 + class:list={["tree-node", first && "first"]} 46 + id={id !== powers.LOCKED && id !== powers.INVALID 47 + ? `astral-power-section-${id.split("#")[1]}` 48 + : undefined} 49 + data-power-id={id} 50 + style={`--deep-children: ${deepChildren}; --depth: ${depth}`} 51 + > 52 + <button 53 + id={id !== powers.LOCKED && id !== powers.INVALID 54 + ? `astral-power-button-${id.split("#")[1]}` 55 + : undefined} 56 + data-power-id={id} 57 + command={id !== powers.LOCKED && id !== powers.INVALID 58 + ? "show-popover" 59 + : undefined} 60 + commandfor={id !== powers.LOCKED && id !== powers.INVALID 61 + ? `astral-power-info-${id.split("#")[1]}` 62 + : undefined} 63 + disabled={id === powers.LOCKED || id === powers.INVALID} 64 + ><img {...image} /></button 65 + > 66 + { 67 + children.length > 0 && ( 68 + <ul style={`--child-count: ${children.length}`}> 69 + {children.map((x, i) => ( 70 + <li style={`--sibling-index: ${i + 1}`}> 71 + <Astro.self {...x} {...powerData[x.id]} /> 72 + </li> 73 + ))} 74 + </ul> 75 + ) 76 + } 77 + <dialog 78 + id={id !== powers.LOCKED && id !== powers.INVALID 79 + ? `astral-power-info-${id.split("#")[1]}` 80 + : undefined} 81 + data-power-id={id} 82 + popover 83 + > 84 + <h1>{name}</h1> 85 + <p>{description}</p> 86 + { 87 + meta.length > 0 && ( 88 + <dl> 89 + {meta.map((x, i) => ( 90 + <> 91 + <dt>{x.name}</dt> <dd>{x.value}</dd> 92 + </> 93 + ))} 94 + </dl> 95 + ) 96 + } 97 + </dialog> 98 + </section> 99 + 100 + <style> 101 + @layer base { 102 + .tree-node { 103 + --image-width-px: 50; 104 + --small-gap-px: 8; 105 + --large-gap-px: 80; 106 + --outline-size-px: 2; 107 + 108 + --image-width: calc(var(--image-width-px) * 1px); 109 + --small-gap: calc(var(--small-gap-px) * 1px); 110 + --large-gap: calc(var(--large-gap-px) * 1px); 111 + --outline-size: calc(var(--outline-size-px) * 1px); 112 + } 113 + 114 + button { 115 + width: var(--image-width); 116 + height: var(--image-width); 117 + padding: 0; 118 + border: 0; 119 + 120 + &, 121 + &:hover { 122 + background-color: CanvasColor; 123 + } 124 + 125 + & img { 126 + width: 100%; 127 + height: 100%; 128 + display: block; 129 + 130 + &:hover { 131 + filter: brightness(150%) contrast(75%); 132 + } 133 + } 134 + } 135 + 136 + ul { 137 + list-style-type: none; 138 + padding: 0; 139 + margin: 0; 140 + } 141 + 142 + dl { 143 + display: grid; 144 + grid-template-columns: auto 1fr; 145 + gap: 0.2ch 1ch; 146 + 147 + dt::after { 148 + content: ": "; 149 + } 150 + 151 + dd { 152 + margin-inline-start: 0; 153 + } 154 + } 155 + } 156 + 157 + @layer position { 158 + .tree-node { 159 + display: flex; 160 + flex-direction: column; 161 + align-items: center; 162 + justify-content: start; 163 + gap: var(--large-gap); 164 + width: min-content; 165 + 166 + margin-inline: auto; 167 + &.first { 168 + margin-block-start: 2rem; 169 + margin-inline: auto; 170 + padding-inline: 1rem; 171 + } 172 + } 173 + 174 + ul { 175 + container-type: size; 176 + display: flex; 177 + flex-direction: row; 178 + gap: var(--small-gap); 179 + 180 + --section-width: max( 181 + calc( 182 + var(--deep-children) * var(--image-width-px) + 183 + (var(--deep-children) - 1) * var(--small-gap-px) 184 + ), 185 + var(--image-width-px) 186 + ); 187 + width: calc(var(--section-width) * 1px); 188 + } 189 + 190 + ul button { 191 + position: relative; 192 + 193 + &::before { 194 + content: ""; 195 + position: absolute; 196 + width: var(--outline-size); 197 + /* offset due to border fucking w things */ 198 + bottom: calc(var(--image-width) - 2 * var(--outline-size)); 199 + 200 + --diff-width: calc( 201 + var(--section-width) / 2 - 202 + ( 203 + var(--section-width) / var(--child-count) * 204 + (var(--sibling-index) - 0.5) 205 + ) 206 + ); 207 + --length: calc( 208 + sqrt(pow(var(--large-gap-px), 2) + pow(var(--diff-width), 2)) * 1px 209 + ); 210 + --angle: calc(atan((var(--diff-width)) / (var(--large-gap-px)))); 211 + 212 + height: var(--length); 213 + transform-origin: bottom center; 214 + rotate: var(--angle); 215 + } 216 + } 217 + 218 + dialog { 219 + max-width: min(60ch, calc(100vw - 10ch)); 220 + margin: auto; 221 + } 222 + } 223 + 224 + section { 225 + --outline: red; 226 + } 227 + 228 + section[data-power-id="dev.vielle.dnd.power#locked"] { 229 + --outline: gold; 230 + } 231 + 232 + section.selected { 233 + --outline: green; 234 + } 235 + 236 + button { 237 + /* doubled since <img> overlaps inner half */ 238 + border: calc(2 * var(--outline-size)) solid var(--outline); 239 + border-radius: 100%; 240 + & img { 241 + border-radius: 100%; 242 + } 243 + } 244 + ul button::before { 245 + background-color: var(--outline); 246 + } 247 + </style>
+20 -74
src/pages/astral/[user].astro
··· 3 3 import { getPdsEndpoint, isAtprotoDid } from "@atcute/identity"; 4 4 import { isHandle } from "@atcute/lexicons/syntax"; 5 5 import Base from "../../Base.astro"; 6 + import Power from "../../components/astral/Power.astro"; 7 + import { powerData, powerTree } from "../../consts/astral"; 6 8 7 9 // slug user to pds 8 10 const { user } = Astro.params; ··· 139 141 res?.points >= res?.powers.length) ?? 140 142 false; 141 143 142 - // custom render 143 - console.log(res, isValidPowers); 144 - if (res) { 145 - const powerUl = document.createElement("dl"); 146 - powerUl.append( 147 - ...(res?.powers 148 - .map((x) => 149 - normalPowers.includes(x as (typeof normalPowers)[number]) 150 - ? (x as powerId) 151 - : powers.INVALID 152 - ) 153 - .map( 154 - (x) => [x, powerData[x]] as [powerId, (typeof powerData)[powerId]] 155 - ) 156 - .map(([id, data]) => { 157 - const dt = document.createElement("dt"); 158 - const dd = document.createElement("dd"); 159 - const dl = document.createElement("dl"); 160 - 161 - console.log(id, data); 162 - 163 - dt.innerText = data.name; 164 - 165 - console.log(id, data); 166 - 167 - const description = [ 168 - document.createElement("dt"), 169 - document.createElement("dd"), 170 - ]; 171 - description[0].innerText = "description"; 172 - description[1].innerText = data.description; 173 - 174 - const img: [HTMLElement, HTMLElement, HTMLImageElement] = [ 175 - document.createElement("dt"), 176 - document.createElement("dd"), 177 - document.createElement("img"), 178 - ]; 179 - img[0].innerText = "Image"; 180 - img[2].src = data.image.src; 181 - img[2].alt = data.image.alt; 182 - img[1].append(img.pop() ?? ""); 183 - 184 - const meta = [ 185 - document.createElement("dt"), 186 - document.createElement("dd"), 187 - document.createElement("dl"), 188 - ]; 189 - meta[0].innerText = "Meta"; 190 - meta[2].append( 191 - ...data.meta 192 - .map((x) => { 193 - const res = [ 194 - document.createElement("dt"), 195 - document.createElement("dd"), 196 - ]; 197 - res[0].innerText = x.name; 198 - res[1].innerText = x.value; 199 - return res; 200 - }) 201 - .flat() 202 - ); 203 - meta[1].append(meta.pop() ?? ""); 204 - 205 - dl.append(...description, ...img, ...meta); 206 - 207 - dd.append(dl); 208 - return [dt, dd]; 209 - }) 210 - .flat() ?? []) 211 - ); 212 - document.body.prepend(powerUl); 213 - } else { 214 - document.body.append("No data for user"); 144 + if (res) 145 + for (const power of res.powers) { 146 + const section = document.querySelector( 147 + `section[data-power-id="${power}"]` 148 + ); 149 + if (!section) continue; 150 + section.classList.add("selected"); 151 + } 152 + </script> 153 + <style is:inline> 154 + body, 155 + html { 156 + width: min-content; 157 + box-sizing: border-box; 158 + margin: 0; 215 159 } 216 - </script> 160 + </style> 161 + 162 + <Power {...powerData[powerTree.id]} {...powerTree} first /> 217 163 </Base>
+63 -152
src/pages/astral/index.astro
··· 1 1 --- 2 2 import Base from "../../Base.astro"; 3 + import Power from "../../components/astral/Power.astro"; 4 + import { powerData, powerTree } from "../../consts/astral"; 3 5 --- 4 6 5 7 <Base title="Astral Powers"> ··· 23 25 powers, 24 26 type powerId, 25 27 normalPowers, 28 + isPowerId, 26 29 } from "../../consts/astral"; 27 - import { DevVielleDndAstral } from "../../lexicons"; 30 + import { DevVielleDndAstral, DevVielleDndPower } from "../../lexicons"; 28 31 const [client, session, did] = await getAuth(true); 29 32 30 33 const res = await client ··· 58 61 return null; 59 62 }); 60 63 64 + if (!res) { 65 + await client 66 + .post("com.atproto.repo.createRecord", { 67 + input: { 68 + repo: did, 69 + collection: "dev.vielle.dnd.astral", 70 + rkey: "self", 71 + record: { 72 + $type: "dev.vielle.dnd.astral", 73 + points: 0, 74 + powers: [], 75 + }, 76 + }, 77 + }) 78 + .then((res) => 79 + res.ok 80 + ? window.location.assign(window.location.pathname) 81 + : console.warn(res.data) 82 + ) 83 + .catch((err) => console.warn(err)); 84 + throw "Failed to create record. Something seriously wrong here..."; 85 + } 86 + 61 87 const isValidPowers = 62 - (res?.powers.reduce( 88 + // all forceSelect are included in powers 89 + Object.entries(powerData) 90 + .filter(([_, { forceSelect }]) => forceSelect) 91 + .reduce((acc, [id]) => acc && res.powers.includes(id), true) && 92 + // all powers are valid ids (power id not of #invalid or #locked) 93 + res.powers.reduce( 63 94 (acc, curr) => 64 95 acc && 96 + isPowerId(curr) && 65 97 curr !== "dev.vielle.dnd.power#invalid" && 66 98 curr !== "dev.vielle.dnd.power#locked", 67 99 true 68 100 ) && 69 - res?.powers.reduce( 70 - (acc, curr) => 71 - acc && 72 - parentPowers[curr].reduce( 73 - (acc, curr) => acc && res.powers.includes(curr), 74 - true 75 - ), 76 - true 77 - ) && 78 - res?.points >= res?.powers.length) ?? 79 - false; 80 - 81 - // custom render 82 - console.log(res, isValidPowers); 83 - 84 - const renderPower = (power: typeof powerTree) => { 85 - const li = document.createElement("li"); 86 - const dl = document.createElement("dl"); 87 - dl.classList.add("power"); 88 - li.append(dl); 89 - 90 - if (power.id === powers.LOCKED) { 91 - dl.innerHTML = `<dt>Status</dt><dd>Locked</dd>`; 92 - return li; 93 - } 94 - 95 - const name = [document.createElement("dt"), document.createElement("dd")]; 96 - name[0].innerText = "name"; 97 - name[1].innerText = powerData[power.id].name; 98 - 99 - const description = [ 100 - document.createElement("dt"), 101 - document.createElement("dd"), 102 - ]; 103 - description[0].innerText = "description"; 104 - description[1].innerText = powerData[power.id].description; 105 - 106 - const img: [HTMLElement, HTMLElement, HTMLImageElement] = [ 107 - document.createElement("dt"), 108 - document.createElement("dd"), 109 - document.createElement("img"), 110 - ]; 111 - img[0].innerText = "Image"; 112 - img[2].src = powerData[power.id].image.src; 113 - img[2].alt = powerData[power.id].image.alt; 114 - img[1].append(img.pop() ?? ""); 101 + // all powers parents are unlocked 102 + res.powers.reduce( 103 + (acc, curr) => 104 + acc && 105 + parentPowers[curr].reduce( 106 + (acc, curr) => acc && res.powers.includes(curr), 107 + true 108 + ), 109 + true 110 + ) && 111 + // number of powers <= points 112 + res.points >= res?.powers.length; 115 113 116 - const meta = [ 117 - document.createElement("dt"), 118 - document.createElement("dd"), 119 - document.createElement("dl"), 120 - ]; 121 - meta[0].innerText = "Meta"; 122 - meta[2].append( 123 - ...powerData[power.id].meta 124 - .map((x) => { 125 - const res = [ 126 - document.createElement("dt"), 127 - document.createElement("dd"), 128 - ]; 129 - res[0].innerText = x.name; 130 - res[1].innerText = x.value; 131 - return res; 132 - }) 133 - .flat() 114 + for (const power of res.powers) { 115 + const section = document.querySelector( 116 + `section[data-power-id="${power}"]` 134 117 ); 135 - meta[1].append(meta.pop() ?? ""); 136 - 137 - const children = [ 138 - document.createElement("dt"), 139 - document.createElement("dd"), 140 - document.createElement("ul"), 141 - ]; 142 - children[0].innerText = "children"; 143 - children[2].append(...power.children.map(renderPower)); 144 - children[1].append(children.pop() ?? ""); 145 - 146 - dl.append(...name, ...img, ...description, ...meta, ...children); 147 - 148 - return li; 149 - }; 150 - 151 - const ul = document.createElement("ul"); 152 - ul.append(renderPower(powerTree)); 153 - document.body.append(ul); 154 - 155 - const powerUl = document.createElement("dl"); 156 - powerUl.append( 157 - ...(res?.powers 158 - .map((x) => 159 - normalPowers.includes(x as (typeof normalPowers)[number]) 160 - ? (x as powerId) 161 - : powers.INVALID 162 - ) 163 - .map((x) => [x, powerData[x]] as [powerId, (typeof powerData)[powerId]]) 164 - .map(([id, data]) => { 165 - const dt = document.createElement("dt"); 166 - const dd = document.createElement("dd"); 167 - const dl = document.createElement("dl"); 118 + if (!section) continue; 119 + section.classList.add("selected"); 120 + } 121 + </script> 122 + <style is:inline> 123 + body, 124 + html { 125 + min-width: 100%; 126 + width: min-content; 127 + box-sizing: border-box; 128 + margin: 0; 129 + } 130 + </style> 168 131 169 - console.log(id, data); 170 - 171 - dt.innerText = data.name; 172 - 173 - console.log(id, data); 174 - 175 - const description = [ 176 - document.createElement("dt"), 177 - document.createElement("dd"), 178 - ]; 179 - description[0].innerText = "description"; 180 - description[1].innerText = data.description; 181 - 182 - const img: [HTMLElement, HTMLElement, HTMLImageElement] = [ 183 - document.createElement("dt"), 184 - document.createElement("dd"), 185 - document.createElement("img"), 186 - ]; 187 - img[0].innerText = "Image"; 188 - img[2].src = data.image.src; 189 - img[2].alt = data.image.alt; 190 - img[1].append(img.pop() ?? ""); 191 - 192 - const meta = [ 193 - document.createElement("dt"), 194 - document.createElement("dd"), 195 - document.createElement("dl"), 196 - ]; 197 - meta[0].innerText = "Meta"; 198 - meta[2].append( 199 - ...data.meta 200 - .map((x) => { 201 - const res = [ 202 - document.createElement("dt"), 203 - document.createElement("dd"), 204 - ]; 205 - res[0].innerText = x.name; 206 - res[1].innerText = x.value; 207 - return res; 208 - }) 209 - .flat() 210 - ); 211 - meta[1].append(meta.pop() ?? ""); 212 - 213 - dl.append(...description, ...img, ...meta); 214 - 215 - dd.append(dl); 216 - return [dt, dd]; 217 - }) 218 - .flat() ?? []) 219 - ); 220 - document.body.prepend(powerUl); 221 - </script> 132 + <Power {...powerData[powerTree.id]} {...powerTree} first /> 222 133 </Base>