Attic is a cozy space with lofty ambitions. attic.social
11
fork

Configure Feed

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

improve handle combobox

+92 -39
+1 -1
src/css/base/global.css
··· 5 5 6 6 body { 7 7 font-family: var(--font-family-1); 8 - font-size: calc(20 / 16 * 1rem); 8 + font-size: var(--font-size-3); 9 9 font-weight: 400; 10 10 font-synthesis: none; 11 11 line-height: 1.5;
+7
src/css/base/properties.css
··· 1 1 :root { 2 2 --font-family-1: "Quantico", sans-serif; 3 3 --font-family-2: "Silkscreen", sans-serif; 4 + 5 + --font-size-1: calc(14 / 16 * 1rem); 6 + --font-size-2: calc(16 / 16 * 1rem); 7 + --font-size-3: calc(20 / 16 * 1rem); 8 + --font-size-4: calc(30 / 16 * 1rem); 9 + --font-size-5: calc(40 / 16 * 1rem); 10 + --font-size-button: calc(24 / 16 * 1rem); 4 11 }
+3 -3
src/css/base/typography.css
··· 1 1 :is(h1, h2, h3) { 2 2 font-family: var(--font-family-1); 3 - font-size: calc(30 / 16 * 1rem); 3 + font-size: var(--font-size-4); 4 4 font-weight: 700; 5 5 line-height: 1.2; 6 6 } 7 7 8 8 h1 { 9 - font-size: calc(40 / 16 * 1rem); 9 + font-size: var(--font-size-5); 10 10 } 11 11 12 12 p:has(small:read-only) { 13 - font-size: calc(16 / 16 * 1rem); 13 + font-size: var(--font-size-2); 14 14 } 15 15 16 16 a[href] {
+3 -2
src/css/components/button.css
··· 1 - button { 1 + button[type] { 2 2 /*background: white;*/ 3 - --font-size: calc(23 / 16 * 1rem); 3 + --font-size: var(--font-size-button); 4 4 border: 15px solid transparent; 5 5 border-image: url("/images/button.svg") 15 fill stretch; 6 6 block-size: calc(var(--font-size) + 30px); ··· 14 14 text-transform: uppercase; 15 15 text-shadow: 2px 2px #40262244; 16 16 transition: border-image 200ms; 17 + 17 18 &:hover { 18 19 border-image-source: url("/images/button-hover.svg"); 19 20 }
+18 -3
src/css/components/form.css
··· 15 15 16 16 & label { 17 17 display: block; 18 - font-size: calc(20 / 16 * 1rem); 18 + font-size: var(--font-size-3); 19 19 font-weight: 700; 20 20 inline-size: fit-content; 21 21 line-height: 1.5; ··· 28 28 &[action*="login"] { 29 29 grid-template-columns: 1fr auto; 30 30 inline-size: min(100%, 600px); 31 + position: relative; 31 32 32 - & input { 33 + &::before { 34 + background: #40262233; 35 + content: "@"; 36 + display: grid; 37 + place-items: center; 38 + inline-size: 35px; 39 + inset-inline-start: calc(anchor(start) + 5px); 40 + inset-block-start: calc(anchor(start) + 5px); 41 + inset-block-end: calc(anchor(end) + 5px); 42 + position: fixed; 43 + position-anchor: --handle; 44 + } 45 + 46 + & > input { 33 47 inline-size: 100%; 34 48 grid-column: 1 / 2; 49 + padding-inline: 30px; 35 50 } 36 51 37 - & button { 52 + & > button { 38 53 grid-column: 2 / 3; 39 54 } 40 55 }
+2 -2
src/css/components/input.css
··· 2 2 /*background: white;*/ 3 3 border: 15px solid transparent; 4 4 border-image: url("/images/input.svg") 15 fill stretch; 5 - block-size: calc((22 / 16 * 1rem) + 30px); 5 + block-size: calc(var(--font-size-button) + 30px); 6 6 font-family: var(--font-family-1); 7 - font-size: calc(18 / 16 * 1rem); 7 + font-size: var(--font-size-3); 8 8 font-weight: 400; 9 9 inline-size: min(100%, 300px); 10 10 line-height: calc(22 / 16 * 1rem);
+13 -4
src/css/main.css
··· 23 23 grid-template-columns: var(--size) auto; 24 24 25 25 & img { 26 - /*border-radius: calc(1px * infinity);*/ 27 26 block-size: var(--size); 28 27 inline-size: var(--size); 29 28 grid-column: 1; ··· 31 30 } 32 31 33 32 & p { 33 + font-size: var(--font-size-3); 34 34 font-weight: 700; 35 35 grid-column: 2; 36 36 grid-row: 2; 37 37 line-height: 1.25; 38 38 39 39 & + & { 40 + font-size: var(--font-size-2); 40 41 font-weight: 400; 41 42 grid-row: 3; 42 43 } ··· 49 50 50 51 #handle-listbox { 51 52 background: white; 52 - border: 2px solid black; 53 + border: 5px solid black; 53 54 display: grid; 54 55 inline-size: anchor-size(--handle inline); 56 + inset-block-start: -5px; 55 57 max-block-size: 230px; 56 58 overflow-x: hidden; 57 59 overflow-y: auto; ··· 62 64 63 65 & .avatar { 64 66 --size: 40px; 65 - padding: 10px; 67 + padding: 5px 10px; 66 68 } 67 69 68 70 & [aria-selected="true"] { 69 71 background: #ffc133; 70 72 } 71 73 72 - & li { 74 + & > p { 75 + background: #40262233; 76 + color: #402622; 77 + font-size: var(--font-size-1); 78 + padding: 5px 10px; 79 + } 80 + 81 + & [role="option"] { 73 82 cursor: pointer; 74 83 75 84 &:hover {
+45 -24
src/routes/+page.svelte
··· 8 8 import type { PageProps } from "./$types"; 9 9 let { data, form }: PageProps = $props(); 10 10 11 + let loginForm: HTMLFormElement | undefined = $state(undefined); 12 + 11 13 let handle = $derived(String(form?.handle ?? "")); 12 14 let handleFocus = $state(false); 13 15 16 + $effect(() => { 17 + if (handle.startsWith("@")) { 18 + handle = handle.substring(1); 19 + } 20 + }); 21 + 14 22 let bskyUsers: BskyUserData[] = $state([]); 15 23 let controller = new AbortController(); 16 24 17 25 let activeOption = $state<HTMLElement | null>(null); 18 26 let handleListbox = $state<HTMLElement | null>(null); 19 27 28 + let timeout: any = $state(); 29 + 30 + const onFocus = () => { 31 + if (timeout) clearTimeout(timeout); 32 + handleFocus = true; 33 + }; 34 + 35 + const onBlur = () => { 36 + timeout = setTimeout(() => (handleFocus = false), 100); 37 + }; 38 + 20 39 const handleKeydown = (ev: KeyboardEvent) => { 21 40 if (!bskyUsers.length || !handleListbox) { 22 41 return; 23 42 } 43 + const options = [ 44 + ...loginForm!.querySelectorAll('[role="option"]'), 45 + ] as HTMLElement[]; 46 + const index = activeOption ? options.indexOf(activeOption) : -1; 24 47 switch (ev.key) { 25 48 case "ArrowDown": 26 49 ev.preventDefault(); 27 - if (activeOption) { 28 - activeOption = 29 - (activeOption.nextElementSibling as HTMLElement) || 30 - (handleListbox.firstElementChild as HTMLElement); 50 + if (index < options.length - 1) { 51 + activeOption = options[index + 1]; 31 52 } else { 32 - activeOption = handleListbox.firstElementChild as HTMLElement; 53 + activeOption = options[0]; 33 54 } 34 55 break; 35 56 case "ArrowUp": 36 57 ev.preventDefault(); 37 - if (activeOption) { 38 - activeOption = 39 - (activeOption.previousElementSibling as HTMLElement) || 40 - (handleListbox.lastElementChild as HTMLElement); 58 + if (index > 0) { 59 + activeOption = options[index - 1]; 41 60 } else { 42 - activeOption = handleListbox.lastElementChild as HTMLElement; 61 + activeOption = options[options.length - 1]; 43 62 } 44 63 break; 45 64 case "Home": 46 65 ev.preventDefault(); 47 - activeOption = handleListbox.firstElementChild as HTMLElement; 66 + activeOption = options[0]; 48 67 break; 49 68 case "End": 50 69 ev.preventDefault(); 51 - activeOption = handleListbox.lastElementChild as HTMLElement; 70 + activeOption = options.at(-1)!; 52 71 break; 53 72 case "Enter": 73 + ev.preventDefault(); 54 74 if (activeOption) { 55 - ev.preventDefault(); 56 75 handle = activeOption.dataset.handle!; 76 + } else if (loginForm) { 77 + loginForm.requestSubmit(); 57 78 } 58 79 break; 59 80 case "Escape": ··· 123 144 <div class="avatar"> 124 145 <img alt="avatar" src="/avatar/{data.user.did}" width="50" height="50" /> 125 146 <p>{data.user.displayName}</p> 126 - <p><small>@{data.user.handle}</small></p> 147 + <p>@{data.user.handle}</p> 127 148 </div> 128 149 <form method="POST" action="?/displayName"> 129 150 <h2>Attic settings</h2> ··· 152 173 <button type="submit">Confirm</button> 153 174 </form> 154 175 {:else} 155 - <form method="POST" action="?/login"> 176 + <form bind:this={loginForm} method="POST" action="?/login"> 156 177 <h2>Sign in</h2> 157 178 <p>Connect with your Atmosphere account.</p> 158 179 {#if form?.action === "login" && form?.error} ··· 164 185 id="handle" 165 186 name="handle" 166 187 bind:value={handle} 167 - onfocus={() => (handleFocus = true)} 168 - onblur={() => setTimeout(() => (handleFocus = false), 100)} 188 + onfocus={onFocus} 189 + onblur={onBlur} 169 190 onkeydown={handleKeydown} 170 191 autocorrect="off" 171 192 spellcheck="false" ··· 176 197 aria-activedescendant={activeOption ? activeOption.id : undefined} 177 198 /> 178 199 {#if handleFocus && bskyUsers.length} 179 - <ul 200 + <div 180 201 bind:this={handleListbox} 181 202 id="handle-listbox" 182 203 role="listbox" 183 204 aria-label="suggestions" 184 205 tabindex="-1" 185 206 > 207 + <p><small>Suggestions provided by Bluesky.</small></p> 186 208 {#each bskyUsers as user (user.did)} 187 - <!-- svelte-ignore a11y_click_events_have_key_events --> 188 - <li 209 + <button 189 210 class="avatar" 190 211 role="option" 212 + tabindex="-1" 191 213 aria-selected={activeOption && 192 214 activeOption.id === `option:${user.did}`} 193 215 id="option:{user.did}" ··· 197 219 }} 198 220 > 199 221 <img alt="avatar" src={user.avatar} width="50" height="50" /> 222 + <p>@{user.handle}</p> 200 223 <p>{user.displayName}</p> 201 - <p><small>@{user.handle}</small></p> 202 - </li> 224 + </button> 203 225 {/each} 204 - </ul> 226 + </div> 205 227 {/if} 206 228 <button type="submit">Sign in</button> 207 - <p><small>Suggestions provided by Bluesky.</small></p> 208 229 </form> 209 230 {/if}