work-in-progress atproto PDS
typescript atproto pds atcute

refactor: improve on handle change dialog

mary.my.id 81105dc5 5481d484

verified
Changed files
+295 -120
packages
danaus
src
web
account
icons
central
styles
+11 -5
packages/danaus/src/web/account/forms.ts
··· 1 - import { signOperation, type UnsignedOperation } from '@atcute/did-plc'; 1 + import { PlcClientError, signOperation, type UnsignedOperation } from '@atcute/did-plc'; 2 2 import type { Did, Handle } from '@atcute/lexicons'; 3 3 import { isHandle } from '@atcute/lexicons/syntax'; 4 4 import { XRPCError } from '@atcute/xrpc-server'; ··· 193 193 194 194 // update PLC document for did:plc accounts 195 195 if (did.startsWith('did:plc:')) { 196 - await updatePlcHandle(ctx, did as Did<'plc'>, handle); 196 + try { 197 + await updatePlcHandle(ctx, did as Did<'plc'>, handle); 198 + } catch (err) { 199 + if (err instanceof PlcClientError) { 200 + invalid(`Unable to update DID document, please try again later`); 201 + } 202 + 203 + throw err; 204 + } 197 205 } 198 206 199 207 // update local database and emit identity event ··· 271 279 services: state.services, 272 280 }; 273 281 274 - // sign with PDS rotation key 282 + // sign with PDS rotation key and submit to PLC directory 275 283 const signedOp = await signOperation(unsignedOp, config.secrets.plcRotationKey); 276 - 277 - // submit to PLC directory 278 284 await plcClient.submitOperation(did, signedOp); 279 285 }
+219 -98
packages/danaus/src/web/account/index.tsx
··· 12 12 import AsideItem from '../admin/components/aside-item.tsx'; 13 13 import { IdProvider } from '../components/id.tsx'; 14 14 import { registerForms } from '../forms/index.ts'; 15 + import AtOutlined from '../icons/central/at-outlined.tsx'; 15 16 import DotGrid1x3HorizontalOutlined from '../icons/central/dot-grid-1x3-horizontal-outlined.tsx'; 16 17 import Key2Outlined from '../icons/central/key-2-outlined.tsx'; 17 18 import PasskeysOutlined from '../icons/central/passkeys-outlined.tsx'; ··· 21 22 import PlusLargeOutlined from '../icons/central/plus-large-outlined.tsx'; 22 23 import ShieldOutlined from '../icons/central/shield-outlined.tsx'; 23 24 import UsbOutlined from '../icons/central/usb-outlined.tsx'; 25 + import AccordionHeader from '../primitives/accordion-header.tsx'; 26 + import AccordionItem from '../primitives/accordion-item.tsx'; 27 + import AccordionPanel from '../primitives/accordion-panel.tsx'; 28 + import Accordion from '../primitives/accordion.tsx'; 24 29 import Button from '../primitives/button.tsx'; 25 30 import DialogActions from '../primitives/dialog-actions.tsx'; 26 31 import DialogBody from '../primitives/dialog-body.tsx'; ··· 151 156 : 'custom'; 152 157 const currentLocalPart = isServiceHandle ? currentHandle.slice(0, -currentDomain.length) : currentHandle; 153 158 159 + const updateHandleError = updateHandleForm.fields.allIssues().at(0); 160 + const refreshHandleError = refreshHandleForm.fields.allIssues().at(0); 161 + 154 162 return c.render( 155 163 <AccountLayout> 156 164 <title>My account - Danaus</title> ··· 160 168 <h3 class="text-base-400 font-medium">Account overview</h3> 161 169 </div> 162 170 171 + {updateHandleError && ( 172 + <MessageBar intent="error" layout="singleline"> 173 + <MessageBarBody>{updateHandleError.message}</MessageBarBody> 174 + </MessageBar> 175 + )} 176 + 177 + {refreshHandleError && ( 178 + <MessageBar intent="error" layout="singleline"> 179 + <MessageBarBody>{refreshHandleError.message}</MessageBarBody> 180 + </MessageBar> 181 + )} 182 + 163 183 <div class="flex flex-col gap-8"> 164 184 <div class="flex flex-col gap-2"> 165 185 <h4 class="text-base-300 font-medium text-neutral-foreground-2">Your identity</h4> ··· 180 200 181 201 <MenuPopover> 182 202 <MenuList> 183 - <Dialog> 184 - <DialogTrigger> 185 - <MenuItem>Change handle</MenuItem> 186 - </DialogTrigger> 187 - 188 - <DialogSurface> 189 - <DialogBody> 190 - <DialogTitle>Change handle</DialogTitle> 203 + <MenuItem command="show-modal" commandfor="change-service-handle-dialog"> 204 + Change handle 205 + </MenuItem> 191 206 192 - <form {...updateHandleForm} class="contents"> 193 - <DialogContent class="flex flex-col gap-4"> 194 - <p class="text-base-300 text-neutral-foreground-3"> 195 - Your handle is your unique identity on the AT Protocol network. 196 - </p> 197 - 198 - <Field 199 - label="Domain" 200 - validationMessageText={ 201 - updateHandleForm.fields.domain.issues()[0]?.message 202 - } 203 - > 204 - <Select 205 - {...updateHandleForm.fields.domain.as('select')} 206 - value={updateHandleForm.fields.domain.value() || currentDomain} 207 - options={[ 208 - ...ctx.config.identity.serviceHandleDomains.map((d) => ({ 209 - value: d, 210 - label: d, 211 - })), 212 - { value: 'custom', label: 'I have my own domain' }, 213 - ]} 214 - /> 215 - </Field> 216 - 217 - <Field 218 - label="Handle" 219 - required 220 - validationMessageText={ 221 - updateHandleForm.fields.handle.issues()[0]?.message 222 - } 223 - > 224 - <Input 225 - {...updateHandleForm.fields.handle.as('text')} 226 - value={updateHandleForm.fields.handle.value() || currentLocalPart} 227 - placeholder="alice" 228 - required 229 - /> 230 - </Field> 231 - 232 - <p class="text-base-200 text-neutral-foreground-3"> 233 - Custom domains must have a DNS TXT record or .well-known file pointing to 234 - your DID. 235 - </p> 236 - </DialogContent> 237 - 238 - <DialogActions> 239 - <DialogClose> 240 - <Button>Cancel</Button> 241 - </DialogClose> 242 - 243 - <Button type="submit" variant="primary"> 244 - Save 245 - </Button> 246 - </DialogActions> 247 - </form> 248 - </DialogBody> 249 - </DialogSurface> 250 - </Dialog> 251 - 252 - <Dialog> 253 - <DialogTrigger> 254 - <MenuItem>Request refresh</MenuItem> 255 - </DialogTrigger> 256 - 257 - <DialogSurface> 258 - <DialogBody> 259 - <DialogTitle>Request handle refresh</DialogTitle> 260 - 261 - <form {...refreshHandleForm} class="contents"> 262 - <DialogContent> 263 - <p class="text-base-300"> 264 - This will notify the network to re-verify your handle. Use this if apps 265 - are marking your handle as invalid despite being set up correctly. 266 - </p> 267 - </DialogContent> 268 - 269 - <DialogActions> 270 - <DialogClose> 271 - <Button>Cancel</Button> 272 - </DialogClose> 273 - 274 - <Button type="submit" variant="primary"> 275 - Refresh 276 - </Button> 277 - </DialogActions> 278 - </form> 279 - </DialogBody> 280 - </DialogSurface> 281 - </Dialog> 207 + <MenuItem command="show-modal" commandfor="refresh-handle-dialog"> 208 + Request refresh 209 + </MenuItem> 282 210 </MenuList> 283 211 </MenuPopover> 284 212 </Menu> ··· 320 248 </div> 321 249 </div> 322 250 </div> 251 + 252 + <Dialog id="change-service-handle-dialog"> 253 + <DialogSurface> 254 + <DialogBody> 255 + <DialogTitle>Change handle</DialogTitle> 256 + 257 + <form {...updateHandleForm} class="contents"> 258 + <DialogContent class="flex flex-col gap-4"> 259 + <p class="text-base-300 text-neutral-foreground-3"> 260 + Your handle is your unique identity on the AT Protocol network. 261 + </p> 262 + 263 + <Field label="Handle" required> 264 + <div class="flex gap-2"> 265 + <Input 266 + {...updateHandleForm.fields.handle.as('text')} 267 + value={updateHandleForm.fields.handle.value() || currentLocalPart} 268 + placeholder="alice" 269 + contentBefore={<AtOutlined size={16} />} 270 + class="grow" 271 + /> 272 + 273 + <Select 274 + {...updateHandleForm.fields.domain.as('select')} 275 + value={updateHandleForm.fields.domain.value() || currentDomain} 276 + options={ctx.config.identity.serviceHandleDomains.map((d) => ({ 277 + value: d, 278 + label: d, 279 + }))} 280 + /> 281 + </div> 282 + </Field> 283 + 284 + <div></div> 285 + </DialogContent> 286 + 287 + <DialogActions> 288 + <Button command="show-modal" commandfor="change-custom-handle-dialog"> 289 + Use my own domain 290 + </Button> 291 + 292 + <div class="grow"></div> 293 + 294 + <DialogClose> 295 + <Button>Cancel</Button> 296 + </DialogClose> 297 + 298 + <Button type="submit" variant="primary"> 299 + Change 300 + </Button> 301 + </DialogActions> 302 + </form> 303 + </DialogBody> 304 + </DialogSurface> 305 + </Dialog> 306 + 307 + <Dialog id="refresh-handle-dialog"> 308 + <DialogSurface> 309 + <DialogBody> 310 + <DialogTitle>Request handle refresh</DialogTitle> 311 + 312 + <form {...refreshHandleForm} class="contents"> 313 + <DialogContent> 314 + <p class="text-base-300"> 315 + This will notify the network to re-verify your handle. Use this if apps are marking your 316 + handle as invalid despite being set up correctly. 317 + </p> 318 + </DialogContent> 319 + 320 + <DialogActions> 321 + <DialogClose> 322 + <Button>Cancel</Button> 323 + </DialogClose> 324 + 325 + <Button type="submit" variant="primary"> 326 + Refresh 327 + </Button> 328 + </DialogActions> 329 + </form> 330 + </DialogBody> 331 + </DialogSurface> 332 + </Dialog> 333 + 334 + <Dialog id="change-custom-handle-dialog"> 335 + <DialogSurface> 336 + <DialogBody> 337 + <DialogTitle>Change handle</DialogTitle> 338 + 339 + <form {...updateHandleForm} class="contents"> 340 + <DialogContent class="flex flex-col gap-4"> 341 + <p class="text-base-300 text-neutral-foreground-3"> 342 + Your handle is your unique identity on the AT Protocol network. 343 + </p> 344 + 345 + <Field label="Handle" required> 346 + <Input 347 + {...updateHandleForm.fields.handle.as('text')} 348 + placeholder="alice.com" 349 + contentBefore={<AtOutlined size={16} />} 350 + /> 351 + </Field> 352 + 353 + <input {...updateHandleForm.fields.domain.as('hidden', 'custom')} /> 354 + 355 + <Accordion class="flex flex-col gap-2"> 356 + <AccordionItem name="handle-method" open> 357 + <AccordionHeader>DNS record</AccordionHeader> 358 + <AccordionPanel> 359 + <div class="flex flex-col gap-3"> 360 + <p class="text-base-300 text-neutral-foreground-3"> 361 + Add the following DNS record to your domain: 362 + </p> 363 + 364 + <div class="flex flex-col gap-2 rounded-md bg-neutral-background-3 p-3"> 365 + <div class="flex flex-col gap-0.5"> 366 + <span class="text-base-200 text-neutral-foreground-3">Host</span> 367 + <input 368 + type="text" 369 + readonly 370 + value="_atproto.<your-domain>" 371 + class="font-mono text-base-300 outline-none" 372 + /> 373 + </div> 374 + <div class="flex flex-col gap-0.5"> 375 + <span class="text-base-200 text-neutral-foreground-3">Type</span> 376 + <input 377 + type="text" 378 + readonly 379 + value="TXT" 380 + class="font-mono text-base-300 outline-none" 381 + /> 382 + </div> 383 + <div class="flex flex-col gap-0.5"> 384 + <span class="text-base-200 text-neutral-foreground-3">Value</span> 385 + <input 386 + type="text" 387 + readonly 388 + value={`did=${session.did}`} 389 + class="font-mono text-base-300 outline-none" 390 + /> 391 + </div> 392 + </div> 393 + </div> 394 + </AccordionPanel> 395 + </AccordionItem> 396 + 397 + <AccordionItem name="handle-method"> 398 + <AccordionHeader>HTTP well-known entry</AccordionHeader> 399 + <AccordionPanel> 400 + <div class="flex flex-col gap-3"> 401 + <p class="text-base-300 text-neutral-foreground-3"> 402 + Upload a text file to the following URL: 403 + </p> 404 + 405 + <div class="flex flex-col gap-2 rounded-md bg-neutral-background-3 p-3"> 406 + <div class="flex flex-col gap-0.5"> 407 + <span class="text-base-200 text-neutral-foreground-3">URL</span> 408 + <input 409 + type="text" 410 + readonly 411 + value="https://<your-domain>/.well-known/atproto-did" 412 + class="font-mono text-base-300 outline-none" 413 + /> 414 + </div> 415 + <div class="flex flex-col gap-0.5"> 416 + <span class="text-base-200 text-neutral-foreground-3">Contents</span> 417 + <input 418 + type="text" 419 + readonly 420 + value={session.did} 421 + class="font-mono text-base-300 outline-none" 422 + /> 423 + </div> 424 + </div> 425 + </div> 426 + </AccordionPanel> 427 + </AccordionItem> 428 + </Accordion> 429 + </DialogContent> 430 + 431 + <DialogActions> 432 + <DialogClose> 433 + <Button>Cancel</Button> 434 + </DialogClose> 435 + 436 + <Button type="submit" variant="primary"> 437 + Change 438 + </Button> 439 + </DialogActions> 440 + </form> 441 + </DialogBody> 442 + </DialogSurface> 443 + </Dialog> 323 444 </AccountLayout>, 324 445 ); 325 446 });
+18
packages/danaus/src/web/icons/central/at-outlined.tsx
··· 1 + import type { IconProps } from './_types.ts'; 2 + 3 + const ArrowInboxOutlined = (props: IconProps) => { 4 + const { size = 24, class: className } = props; 5 + 6 + return ( 7 + <svg viewBox="0 0 24 24" width={size} height={size} fill="none" class={className}> 8 + <path 9 + d="M16.7368 19.6541C15.361 20.5073 13.738 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 13.9262 20.0428 15.9154 17.8101 15.7125C15.9733 15.5455 14.6512 13.8737 14.9121 12.0479L15.4274 8.5M14.8581 12.4675C14.559 14.596 12.8066 16.1093 10.9442 15.8476C9.08175 15.5858 7.81444 13.6481 8.11358 11.5196C8.41272 9.39109 10.165 7.87778 12.0275 8.13953C13.8899 8.40128 15.1573 10.339 14.8581 12.4675Z" 10 + stroke="currentColor" 11 + stroke-width="2" 12 + stroke-linecap="round" 13 + /> 14 + </svg> 15 + ); 16 + }; 17 + 18 + export default ArrowInboxOutlined;
+47 -17
packages/danaus/src/web/styles/main.out.css
··· 322 322 .-mx-1\.25 { 323 323 margin-inline: calc(var(--spacing) * -1.25); 324 324 } 325 + .-mx-3 { 326 + margin-inline: calc(var(--spacing) * -3); 327 + } 325 328 .-my-0\.5 { 326 329 margin-block: calc(var(--spacing) * -0.5); 327 330 } ··· 416 419 .min-h-9 { 417 420 min-height: calc(var(--spacing) * 9); 418 421 } 422 + .min-h-11 { 423 + min-height: calc(var(--spacing) * 11); 424 + } 419 425 .min-h-dvh { 420 426 min-height: 100dvh; 421 427 } ··· 479 485 .flex-1 { 480 486 flex: 1; 481 487 } 488 + .shrink { 489 + flex-shrink: 1; 490 + } 482 491 .shrink-0 { 483 492 flex-shrink: 0; 484 493 } 485 494 .grow { 486 495 flex-grow: 1; 496 + } 497 + .basis-0 { 498 + flex-basis: calc(var(--spacing) * 0); 487 499 } 488 500 .cursor-pointer { 489 501 cursor: pointer; 502 + } 503 + .list-none { 504 + list-style-type: none; 490 505 } 491 506 .appearance-none { 492 507 appearance: none; ··· 503 518 .flex-col { 504 519 flex-direction: column; 505 520 } 521 + .flex-row-reverse { 522 + flex-direction: row-reverse; 523 + } 506 524 .flex-nowrap { 507 525 flex-wrap: nowrap; 508 526 } ··· 610 628 .border { 611 629 border-style: var(--tw-border-style); 612 630 border-width: 1px; 631 + } 632 + .border-0 { 633 + border-style: var(--tw-border-style); 634 + border-width: 0px; 613 635 } 614 636 .border-b { 615 637 border-bottom-style: var(--tw-border-style); ··· 754 776 .pb-2 { 755 777 padding-bottom: calc(var(--spacing) * 2); 756 778 } 779 + .pb-3 { 780 + padding-bottom: calc(var(--spacing) * 3); 781 + } 757 782 .pl-1 { 758 783 padding-left: calc(var(--spacing) * 1); 759 784 } ··· 807 832 .font-semibold { 808 833 --tw-font-weight: var(--font-weight-semibold); 809 834 font-weight: var(--font-weight-semibold); 835 + } 836 + .wrap-anywhere { 837 + overflow-wrap: anywhere; 810 838 } 811 839 .wrap-break-word { 812 840 overflow-wrap: break-word; ··· 880 908 transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); 881 909 transition-duration: var(--tw-duration, var(--default-transition-duration)); 882 910 } 911 + .transition-transform { 912 + transition-property: transform, translate, scale, rotate; 913 + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); 914 + transition-duration: var(--tw-duration, var(--default-transition-duration)); 915 + } 883 916 .duration-100 { 884 917 --tw-duration: 100ms; 885 918 transition-duration: 100ms; ··· 891 924 .outline-none { 892 925 --tw-outline-style: none; 893 926 outline-style: none; 927 + } 928 + .select-all { 929 + -webkit-user-select: all; 930 + user-select: all; 894 931 } 895 932 .select-none { 896 933 -webkit-user-select: none; ··· 913 950 } 914 951 .try-flip-y { 915 952 position-try-fallbacks: flip-block; 953 + } 954 + .group-open\/accordion-item\:rotate-180 { 955 + &:is(:where(.group\/accordion-item):is([open], :popover-open, :open) *) { 956 + rotate: 180deg; 957 + } 916 958 } 917 959 .group-hover\/checkbox\:border-compound-brand-background-hover { 918 960 &:is(:where(.group\/checkbox):hover *) { ··· 1107 1149 display: flex; 1108 1150 } 1109 1151 } 1110 - .open\:items-end { 1111 - &:is([open], :popover-open, :open) { 1112 - align-items: flex-end; 1113 - } 1114 - } 1115 - .open\:justify-center { 1116 - &:is([open], :popover-open, :open) { 1117 - justify-content: center; 1118 - } 1119 - } 1120 1152 .hover\:border-neutral-stroke-1-hover { 1121 1153 &:hover { 1122 1154 @media (hover: hover) { ··· 1375 1407 padding-top: calc(var(--spacing) * 24); 1376 1408 } 1377 1409 } 1378 - .open\:sm\:items-center { 1379 - &:is([open], :popover-open, :open) { 1380 - @media (width >= 40rem) { 1381 - align-items: center; 1382 - } 1383 - } 1384 - } 1385 1410 .lg\:grid { 1386 1411 @media (width >= 64rem) { 1387 1412 display: grid; ··· 1420 1445 .\@sm\/dialog-body\:justify-start { 1421 1446 @container dialog-body (width >= 24rem) { 1422 1447 justify-content: flex-start; 1448 + } 1449 + } 1450 + .\[\&\:\:-webkit-details-marker\]\:hidden { 1451 + &::-webkit-details-marker { 1452 + display: none; 1423 1453 } 1424 1454 } 1425 1455 }