work-in-progress atproto PDS
typescript atproto pds atcute
4
fork

Configure Feed

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

refactor: move primitives into barrel files

Mary c1991119 1b5e00aa

+556 -562
+1 -1
packages/danaus/src/accounts/db/schema.ts
··· 1 1 import type { Did, Handle } from '@atcute/lexicons/syntax'; 2 2 3 - import { sql } from 'drizzle-orm'; 4 3 import type { AuthenticatorTransportFuture } from '@simplewebauthn/server'; 4 + import { sql } from 'drizzle-orm'; 5 5 import { 6 6 blob, 7 7 foreignKey,
+1 -2
packages/danaus/src/accounts/manager.ts
··· 5 5 import { isDid, isHandle } from '@atcute/lexicons/syntax'; 6 6 import { InvalidRequestError, UpstreamFailureError } from '@atcute/xrpc-server'; 7 7 8 + import type { AuthenticatorTransportFuture } from '@simplewebauthn/server'; 8 9 import { and, asc, eq, gt, inArray, isNotNull, isNull, like, lte, or, sql } from 'drizzle-orm'; 9 10 import { nanoid } from 'nanoid'; 10 11 ··· 14 15 import { TimeKeyset } from '#app/utils/keyset.ts'; 15 16 import { DAY, HOUR } from '#app/utils/times.ts'; 16 17 import { generateAppPassword, generateInviteCode } from '#app/utils/token.ts'; 17 - 18 - import type { AuthenticatorTransportFuture } from '@simplewebauthn/server'; 19 18 20 19 import { getAccountDb, t, type AccountDb } from './db'; 21 20 import { AppPasswordPrivilege, EmailTokenPurpose, PreferredMfa, WebAuthnCredentialType } from './db/schema';
+2 -6
packages/danaus/src/accounts/webauthn.ts
··· 42 42 * @param params registration parameters 43 43 * @returns registration options to send to the client 44 44 */ 45 - export const generateWebAuthnRegistrationOptions = async ( 46 - params: GenerateRegistrationOptionsParams, 47 - ) => { 45 + export const generateWebAuthnRegistrationOptions = async (params: GenerateRegistrationOptionsParams) => { 48 46 const { rpId, rpName, userId, userName, excludeCredentials = [] } = params; 49 47 50 48 return await generateRegistrationOptions({ ··· 111 109 * @param params authentication parameters 112 110 * @returns authentication options to send to the client 113 111 */ 114 - export const generateWebAuthnAuthenticationOptions = async ( 115 - params: GenerateAuthenticationOptionsParams, 116 - ) => { 112 + export const generateWebAuthnAuthenticationOptions = async (params: GenerateAuthenticationOptionsParams) => { 117 113 const { rpId, allowCredentials = [] } = params; 118 114 119 115 return await generateAuthenticationOptions({
+124 -147
packages/danaus/src/web/controllers/account.tsx
··· 10 10 deleteAppPasswordForm, 11 11 refreshHandleForm, 12 12 updateHandleForm, 13 - } from '../account/forms.ts'; 14 - import AtOutlined from '../icons/central/at-outlined.tsx'; 15 - import DotGrid1x3HorizontalOutlined from '../icons/central/dot-grid-1x3-horizontal-outlined.tsx'; 16 - import Key2Outlined from '../icons/central/key-2-outlined.tsx'; 17 - import PlusLargeOutlined from '../icons/central/plus-large-outlined.tsx'; 18 - import { AccountLayout } from '../layouts/account.tsx'; 19 - import { getAppContext } from '../middlewares/app-context.ts'; 20 - import { getSession, requireSession } from '../middlewares/session.ts'; 21 - import AccordionHeader from '../primitives/accordion-header.tsx'; 22 - import AccordionItem from '../primitives/accordion-item.tsx'; 23 - import AccordionPanel from '../primitives/accordion-panel.tsx'; 24 - import Accordion from '../primitives/accordion.tsx'; 25 - import Button from '../primitives/button.tsx'; 26 - import DialogActions from '../primitives/dialog-actions.tsx'; 27 - import DialogBody from '../primitives/dialog-body.tsx'; 28 - import DialogClose from '../primitives/dialog-close.tsx'; 29 - import DialogContent from '../primitives/dialog-content.tsx'; 30 - import DialogSurface from '../primitives/dialog-surface.tsx'; 31 - import DialogTitle from '../primitives/dialog-title.tsx'; 32 - import DialogTrigger from '../primitives/dialog-trigger.tsx'; 33 - import Dialog from '../primitives/dialog.tsx'; 34 - import Field from '../primitives/field.tsx'; 35 - import Input from '../primitives/input.tsx'; 36 - import MenuItem from '../primitives/menu-item.tsx'; 37 - import MenuList from '../primitives/menu-list.tsx'; 38 - import MenuPopover from '../primitives/menu-popover.tsx'; 39 - import MenuTrigger from '../primitives/menu-trigger.tsx'; 40 - import Menu from '../primitives/menu.tsx'; 41 - import MessageBarBody from '../primitives/message-bar-body.tsx'; 42 - import MessageBarTitle from '../primitives/message-bar-title.tsx'; 43 - import MessageBar from '../primitives/message-bar.tsx'; 44 - import Select from '../primitives/select.tsx'; 45 - import type { routes } from '../routes.ts'; 13 + } from '#web/account/forms.ts'; 14 + import AtOutlined from '#web/icons/central/at-outlined.tsx'; 15 + import DotGrid1x3HorizontalOutlined from '#web/icons/central/dot-grid-1x3-horizontal-outlined.tsx'; 16 + import Key2Outlined from '#web/icons/central/key-2-outlined.tsx'; 17 + import PlusLargeOutlined from '#web/icons/central/plus-large-outlined.tsx'; 18 + import { AccountLayout } from '#web/layouts/account.tsx'; 19 + import { getAppContext } from '#web/middlewares/app-context.ts'; 20 + import { getSession, requireSession } from '#web/middlewares/session.ts'; 21 + import { Accordion, Button, Dialog, Field, Input, Menu, MessageBar, Select } from '#web/primitives/index.ts'; 22 + import type { routes } from '#web/routes.ts'; 46 23 47 24 import security from './account/security.tsx'; 48 25 ··· 85 62 </div> 86 63 87 64 {updateHandleError && ( 88 - <MessageBar intent="error" layout="singleline"> 89 - <MessageBarBody>{updateHandleError.message}</MessageBarBody> 90 - </MessageBar> 65 + <MessageBar.Root intent="error" layout="singleline"> 66 + <MessageBar.Body>{updateHandleError.message}</MessageBar.Body> 67 + </MessageBar.Root> 91 68 )} 92 69 93 70 {refreshHandleError && ( 94 - <MessageBar intent="error" layout="singleline"> 95 - <MessageBarBody>{refreshHandleError.message}</MessageBarBody> 96 - </MessageBar> 71 + <MessageBar.Root intent="error" layout="singleline"> 72 + <MessageBar.Body>{refreshHandleError.message}</MessageBar.Body> 73 + </MessageBar.Root> 97 74 )} 98 75 99 76 <div class="flex flex-col gap-8"> ··· 107 84 <p class="text-base-300 text-neutral-foreground-3">Your username on the network</p> 108 85 </div> 109 86 110 - <Menu> 111 - <MenuTrigger> 87 + <Menu.Root> 88 + <Menu.Trigger> 112 89 <button class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-subtle-background text-neutral-foreground-3 outline-2 -outline-offset-2 outline-transparent transition hover:bg-subtle-background-hover hover:text-neutral-foreground-3-hover focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active active:text-neutral-foreground-3-active"> 113 90 <DotGrid1x3HorizontalOutlined size={16} /> 114 91 </button> 115 - </MenuTrigger> 92 + </Menu.Trigger> 116 93 117 - <MenuPopover> 118 - <MenuList> 119 - <MenuItem command="show-modal" commandfor="change-service-handle-dialog"> 94 + <Menu.Popover> 95 + <Menu.List> 96 + <Menu.Item command="show-modal" commandfor="change-service-handle-dialog"> 120 97 Change handle 121 - </MenuItem> 98 + </Menu.Item> 122 99 123 - <MenuItem command="show-modal" commandfor="refresh-handle-dialog"> 100 + <Menu.Item command="show-modal" commandfor="refresh-handle-dialog"> 124 101 Request refresh 125 - </MenuItem> 126 - </MenuList> 127 - </MenuPopover> 128 - </Menu> 102 + </Menu.Item> 103 + </Menu.List> 104 + </Menu.Popover> 105 + </Menu.Root> 129 106 </div> 130 107 </div> 131 108 </div> ··· 167 144 </div> 168 145 </div> 169 146 170 - <Dialog id="change-service-handle-dialog"> 171 - <DialogSurface> 172 - <DialogBody> 173 - <DialogTitle>Change handle</DialogTitle> 147 + <Dialog.Root id="change-service-handle-dialog"> 148 + <Dialog.Surface> 149 + <Dialog.Body> 150 + <Dialog.Title>Change handle</Dialog.Title> 174 151 175 152 <form {...updateHandleForm} class="contents"> 176 - <DialogContent class="flex flex-col gap-4"> 153 + <Dialog.Content class="flex flex-col gap-4"> 177 154 <p class="text-base-300 text-neutral-foreground-3"> 178 155 Your handle is your unique identity on the AT Protocol network. 179 156 </p> ··· 198 175 /> 199 176 </div> 200 177 </Field> 201 - </DialogContent> 178 + </Dialog.Content> 202 179 203 - <DialogActions> 180 + <Dialog.Actions> 204 181 <Button command="show-modal" commandfor="change-custom-handle-dialog"> 205 182 Use my own domain 206 183 </Button> 207 184 208 185 <div class="grow"></div> 209 186 210 - <DialogClose> 187 + <Dialog.Close> 211 188 <Button>Cancel</Button> 212 - </DialogClose> 189 + </Dialog.Close> 213 190 214 191 <Button type="submit" variant="primary"> 215 192 Change 216 193 </Button> 217 - </DialogActions> 194 + </Dialog.Actions> 218 195 </form> 219 - </DialogBody> 220 - </DialogSurface> 221 - </Dialog> 196 + </Dialog.Body> 197 + </Dialog.Surface> 198 + </Dialog.Root> 222 199 223 - <Dialog id="refresh-handle-dialog"> 224 - <DialogSurface> 225 - <DialogBody> 226 - <DialogTitle>Request handle refresh</DialogTitle> 200 + <Dialog.Root id="refresh-handle-dialog"> 201 + <Dialog.Surface> 202 + <Dialog.Body> 203 + <Dialog.Title>Request handle refresh</Dialog.Title> 227 204 228 205 <form {...refreshHandleForm} class="contents"> 229 - <DialogContent> 206 + <Dialog.Content> 230 207 <p class="text-base-300"> 231 208 This will notify the network to re-verify your handle. Use this if apps are marking your 232 209 handle as invalid despite being set up correctly. 233 210 </p> 234 - </DialogContent> 211 + </Dialog.Content> 235 212 236 - <DialogActions> 237 - <DialogClose> 213 + <Dialog.Actions> 214 + <Dialog.Close> 238 215 <Button>Cancel</Button> 239 - </DialogClose> 216 + </Dialog.Close> 240 217 241 218 <Button type="submit" variant="primary"> 242 219 Refresh 243 220 </Button> 244 - </DialogActions> 221 + </Dialog.Actions> 245 222 </form> 246 - </DialogBody> 247 - </DialogSurface> 248 - </Dialog> 223 + </Dialog.Body> 224 + </Dialog.Surface> 225 + </Dialog.Root> 249 226 250 - <Dialog id="change-custom-handle-dialog"> 251 - <DialogSurface> 252 - <DialogBody> 253 - <DialogTitle>Change handle</DialogTitle> 227 + <Dialog.Root id="change-custom-handle-dialog"> 228 + <Dialog.Surface> 229 + <Dialog.Body> 230 + <Dialog.Title>Change handle</Dialog.Title> 254 231 255 232 <form {...updateHandleForm} class="contents"> 256 - <DialogContent class="flex flex-col gap-4"> 233 + <Dialog.Content class="flex flex-col gap-4"> 257 234 <p class="text-base-300 text-neutral-foreground-3"> 258 235 Your handle is your unique identity on the AT Protocol network. 259 236 </p> ··· 268 245 269 246 <input {...updateHandleForm.fields.domain.as('hidden', 'custom')} /> 270 247 271 - <Accordion class="flex flex-col gap-2"> 272 - <AccordionItem name="handle-method" open> 273 - <AccordionHeader>DNS record</AccordionHeader> 274 - <AccordionPanel> 248 + <Accordion.Root class="flex flex-col gap-2"> 249 + <Accordion.Item name="handle-method" open> 250 + <Accordion.Header>DNS record</Accordion.Header> 251 + <Accordion.Panel> 275 252 <div class="flex flex-col gap-3"> 276 253 <p class="text-base-300 text-neutral-foreground-3"> 277 254 Add the following DNS record to your domain: ··· 307 284 </div> 308 285 </div> 309 286 </div> 310 - </AccordionPanel> 311 - </AccordionItem> 287 + </Accordion.Panel> 288 + </Accordion.Item> 312 289 313 - <AccordionItem name="handle-method"> 314 - <AccordionHeader>HTTP well-known entry</AccordionHeader> 315 - <AccordionPanel> 290 + <Accordion.Item name="handle-method"> 291 + <Accordion.Header>HTTP well-known entry</Accordion.Header> 292 + <Accordion.Panel> 316 293 <div class="flex flex-col gap-3"> 317 294 <p class="text-base-300 text-neutral-foreground-3"> 318 295 Upload a text file to the following URL: ··· 339 316 </div> 340 317 </div> 341 318 </div> 342 - </AccordionPanel> 343 - </AccordionItem> 344 - </Accordion> 345 - </DialogContent> 319 + </Accordion.Panel> 320 + </Accordion.Item> 321 + </Accordion.Root> 322 + </Dialog.Content> 346 323 347 - <DialogActions> 348 - <DialogClose> 324 + <Dialog.Actions> 325 + <Dialog.Close> 349 326 <Button>Cancel</Button> 350 - </DialogClose> 327 + </Dialog.Close> 351 328 352 329 <Button type="submit" variant="primary"> 353 330 Change 354 331 </Button> 355 - </DialogActions> 332 + </Dialog.Actions> 356 333 </form> 357 - </DialogBody> 358 - </DialogSurface> 359 - </Dialog> 334 + </Dialog.Body> 335 + </Dialog.Surface> 336 + </Dialog.Root> 360 337 </AccountLayout>, 361 338 ); 362 339 }, ··· 386 363 </div> 387 364 388 365 {newPasswordResult && ( 389 - <MessageBar intent="success" layout="multiline"> 390 - <MessageBarBody> 391 - <MessageBarTitle>App password created</MessageBarTitle> 366 + <MessageBar.Root intent="success" layout="multiline"> 367 + <MessageBar.Body> 368 + <MessageBar.Title>App password created</MessageBar.Title> 392 369 393 370 <div class="mt-2 flex flex-col gap-2"> 394 371 <code class="rounded-md bg-neutral-background-3 px-2 py-1 font-mono text-base-300"> ··· 398 375 Copy this password now. You won't be able to see it again. 399 376 </p> 400 377 </div> 401 - </MessageBarBody> 402 - </MessageBar> 378 + </MessageBar.Body> 379 + </MessageBar.Root> 403 380 )} 404 381 405 382 {newPasswordError && ( 406 - <MessageBar intent="error" layout="singleline"> 407 - <MessageBarBody>{newPasswordError.message}</MessageBarBody> 408 - </MessageBar> 383 + <MessageBar.Root intent="error" layout="singleline"> 384 + <MessageBar.Body>{newPasswordError.message}</MessageBar.Body> 385 + </MessageBar.Root> 409 386 )} 410 387 411 388 <div class="flex flex-col divide-y divide-neutral-stroke-2 rounded-md bg-neutral-background-1 shadow-4"> ··· 446 423 </p> 447 424 </div> 448 425 449 - <Menu> 450 - <MenuTrigger> 426 + <Menu.Root> 427 + <Menu.Trigger> 451 428 <button class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-subtle-background text-neutral-foreground-3 outline-2 -outline-offset-2 outline-transparent transition hover:bg-subtle-background-hover hover:text-neutral-foreground-3-hover focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active active:text-neutral-foreground-3-active"> 452 429 <DotGrid1x3HorizontalOutlined size={16} /> 453 430 </button> 454 - </MenuTrigger> 431 + </Menu.Trigger> 455 432 456 - <MenuPopover> 457 - <MenuList> 458 - <Dialog> 459 - <DialogTrigger> 460 - <MenuItem>Remove</MenuItem> 461 - </DialogTrigger> 433 + <Menu.Popover> 434 + <Menu.List> 435 + <Dialog.Root> 436 + <Dialog.Trigger> 437 + <Menu.Item>Remove</Menu.Item> 438 + </Dialog.Trigger> 462 439 463 - <DialogSurface> 464 - <DialogBody> 465 - <DialogTitle>Remove app password?</DialogTitle> 440 + <Dialog.Surface> 441 + <Dialog.Body> 442 + <Dialog.Title>Remove app password?</Dialog.Title> 466 443 467 444 <form {...deleteAppPasswordForm} class="contents"> 468 445 <input type="hidden" name="name" value={password.name} /> 469 446 470 - <DialogContent> 447 + <Dialog.Content> 471 448 <p class="text-base-300"> 472 449 Any app signed in with "{password.name}" will be signed out immediately. 473 450 </p> 474 - </DialogContent> 451 + </Dialog.Content> 475 452 476 - <DialogActions> 477 - <DialogClose> 453 + <Dialog.Actions> 454 + <Dialog.Close> 478 455 <Button>Cancel</Button> 479 - </DialogClose> 456 + </Dialog.Close> 480 457 481 458 <Button type="submit" variant="primary"> 482 459 Remove 483 460 </Button> 484 - </DialogActions> 461 + </Dialog.Actions> 485 462 </form> 486 - </DialogBody> 487 - </DialogSurface> 488 - </Dialog> 489 - </MenuList> 490 - </MenuPopover> 491 - </Menu> 463 + </Dialog.Body> 464 + </Dialog.Surface> 465 + </Dialog.Root> 466 + </Menu.List> 467 + </Menu.Popover> 468 + </Menu.Root> 492 469 </div> 493 470 ); 494 471 })} 495 472 </div> 496 473 </div> 497 474 498 - <Dialog id="create-app-password-dialog"> 499 - <DialogSurface> 500 - <DialogBody> 501 - <DialogTitle>Create app password</DialogTitle> 475 + <Dialog.Root id="create-app-password-dialog"> 476 + <Dialog.Surface> 477 + <Dialog.Body> 478 + <Dialog.Title>Create app password</Dialog.Title> 502 479 503 480 <form {...createAppPasswordForm} class="contents"> 504 - <DialogContent class="flex flex-col gap-4"> 481 + <Dialog.Content class="flex flex-col gap-4"> 505 482 <p class="text-base-300 text-neutral-foreground-3"> 506 483 App passwords let you sign into legacy AT Protocol apps without giving them access to 507 484 your main password. ··· 521 498 ]} 522 499 /> 523 500 </Field> 524 - </DialogContent> 501 + </Dialog.Content> 525 502 526 - <DialogActions> 527 - <DialogClose> 503 + <Dialog.Actions> 504 + <Dialog.Close> 528 505 <Button>Cancel</Button> 529 - </DialogClose> 506 + </Dialog.Close> 530 507 531 508 <Button type="submit" variant="primary"> 532 509 Create 533 510 </Button> 534 - </DialogActions> 511 + </Dialog.Actions> 535 512 </form> 536 - </DialogBody> 537 - </DialogSurface> 538 - </Dialog> 513 + </Dialog.Body> 514 + </Dialog.Surface> 515 + </Dialog.Root> 539 516 </AccountLayout>, 540 517 ); 541 518 },
+74 -82
packages/danaus/src/web/controllers/account/security/overview.tsx
··· 1 1 import type { BuildAction } from '@oomfware/fetch-router'; 2 2 import { render } from '@oomfware/jsx'; 3 3 4 - import type { Account, TotpCredential, WebauthnCredential } from '#app/accounts/manager.ts'; 5 4 import { WebAuthnCredentialType } from '#app/accounts/db/schema.ts'; 5 + import type { Account, TotpCredential, WebauthnCredential } from '#app/accounts/manager.ts'; 6 6 7 7 import DotGrid1x3HorizontalOutlined from '#web/icons/central/dot-grid-1x3-horizontal-outlined.tsx'; 8 8 import PasskeysOutlined from '#web/icons/central/passkeys-outlined.tsx'; ··· 13 13 import { AccountLayout } from '#web/layouts/account.tsx'; 14 14 import { getAppContext } from '#web/middlewares/app-context.ts'; 15 15 import { getSession } from '#web/middlewares/session.ts'; 16 - import Button from '#web/primitives/button.tsx'; 17 - import DialogActions from '#web/primitives/dialog-actions.tsx'; 18 - import DialogBody from '#web/primitives/dialog-body.tsx'; 19 - import DialogClose from '#web/primitives/dialog-close.tsx'; 20 - import DialogContent from '#web/primitives/dialog-content.tsx'; 21 - import DialogSurface from '#web/primitives/dialog-surface.tsx'; 22 - import DialogTitle from '#web/primitives/dialog-title.tsx'; 23 - import Dialog from '#web/primitives/dialog.tsx'; 24 - import MenuDivider from '#web/primitives/menu-divider.tsx'; 25 - import MenuItem from '#web/primitives/menu-item.tsx'; 26 - import MenuList from '#web/primitives/menu-list.tsx'; 27 - import MenuPopover from '#web/primitives/menu-popover.tsx'; 28 - import MenuTrigger from '#web/primitives/menu-trigger.tsx'; 29 - import Menu from '#web/primitives/menu.tsx'; 16 + import { Button, Dialog, Menu } from '#web/primitives/index.ts'; 30 17 import { routes } from '#web/routes.ts'; 31 18 32 19 export default { ··· 37 24 const account = accountManager.getAccount(did)!; 38 25 39 26 const totpCredentials = accountManager.listTotpCredentials(did); 40 - const securityKeys = accountManager.listWebAuthnCredentialsByType(did, WebAuthnCredentialType.SecurityKey); 27 + const securityKeys = accountManager.listWebAuthnCredentialsByType( 28 + did, 29 + WebAuthnCredentialType.SecurityKey, 30 + ); 41 31 const hasMfa = totpCredentials.length > 0 || securityKeys.length > 0; 42 32 43 33 return render( ··· 82 72 83 73 {!account?.email_confirmed_at && <Button>Verify</Button>} 84 74 85 - <Menu> 86 - <MenuTrigger> 75 + <Menu.Root> 76 + <Menu.Trigger> 87 77 <button class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-subtle-background text-neutral-foreground-3 outline-2 -outline-offset-2 outline-transparent transition hover:bg-subtle-background-hover hover:text-neutral-foreground-3-hover focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active active:text-neutral-foreground-3-active"> 88 78 <DotGrid1x3HorizontalOutlined size={16} /> 89 79 </button> 90 - </MenuTrigger> 80 + </Menu.Trigger> 91 81 92 - <MenuPopover> 93 - <MenuList> 94 - <MenuItem>Change email</MenuItem> 95 - </MenuList> 96 - </MenuPopover> 97 - </Menu> 82 + <Menu.Popover> 83 + <Menu.List> 84 + <Menu.Item>Change email</Menu.Item> 85 + </Menu.List> 86 + </Menu.Popover> 87 + </Menu.Root> 98 88 </div> 99 89 </div> 100 90 </div> ··· 126 116 </p> 127 117 </div> 128 118 129 - <Menu> 130 - <MenuTrigger> 119 + <Menu.Root> 120 + <Menu.Trigger> 131 121 <button class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-subtle-background text-neutral-foreground-3 outline-2 -outline-offset-2 outline-transparent transition hover:bg-subtle-background-hover hover:text-neutral-foreground-3-hover focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active active:text-neutral-foreground-3-active"> 132 122 <DotGrid1x3HorizontalOutlined size={16} /> 133 123 </button> 134 - </MenuTrigger> 124 + </Menu.Trigger> 135 125 136 - <MenuPopover> 137 - <MenuList> 138 - <MenuItem>Change password</MenuItem> 139 - </MenuList> 140 - </MenuPopover> 141 - </Menu> 126 + <Menu.Popover> 127 + <Menu.List> 128 + <Menu.Item>Change password</Menu.Item> 129 + </Menu.List> 130 + </Menu.Popover> 131 + </Menu.Root> 142 132 </div> 143 133 144 134 {/* TOTP credentials */} ··· 153 143 </p> 154 144 </div> 155 145 156 - <Menu> 157 - <MenuTrigger> 146 + <Menu.Root> 147 + <Menu.Trigger> 158 148 <button class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-subtle-background text-neutral-foreground-3 outline-2 -outline-offset-2 outline-transparent transition hover:bg-subtle-background-hover hover:text-neutral-foreground-3-hover focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active active:text-neutral-foreground-3-active"> 159 149 <DotGrid1x3HorizontalOutlined size={16} /> 160 150 </button> 161 - </MenuTrigger> 151 + </Menu.Trigger> 162 152 163 - <MenuPopover> 164 - <MenuList> 165 - <MenuItem href={routes.account.security.totp.remove.href({ id: totp.id })}>Remove</MenuItem> 166 - </MenuList> 167 - </MenuPopover> 168 - </Menu> 153 + <Menu.Popover> 154 + <Menu.List> 155 + <Menu.Item href={routes.account.security.totp.remove.href({ id: totp.id })}> 156 + Remove 157 + </Menu.Item> 158 + </Menu.List> 159 + </Menu.Popover> 160 + </Menu.Root> 169 161 </div> 170 162 ))} 171 163 ··· 181 173 </p> 182 174 </div> 183 175 184 - <Menu> 185 - <MenuTrigger> 176 + <Menu.Root> 177 + <Menu.Trigger> 186 178 <button class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-subtle-background text-neutral-foreground-3 outline-2 -outline-offset-2 outline-transparent transition hover:bg-subtle-background-hover hover:text-neutral-foreground-3-hover focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active active:text-neutral-foreground-3-active"> 187 179 <DotGrid1x3HorizontalOutlined size={16} /> 188 180 </button> 189 - </MenuTrigger> 181 + </Menu.Trigger> 190 182 191 - <MenuPopover> 192 - <MenuList> 193 - <MenuItem href={routes.account.security.webauthn.remove.href({ id: key.id })}> 183 + <Menu.Popover> 184 + <Menu.List> 185 + <Menu.Item href={routes.account.security.webauthn.remove.href({ id: key.id })}> 194 186 Remove 195 - </MenuItem> 196 - </MenuList> 197 - </MenuPopover> 198 - </Menu> 187 + </Menu.Item> 188 + </Menu.List> 189 + </Menu.Popover> 190 + </Menu.Root> 199 191 </div> 200 192 ))} 201 193 ··· 217 209 </button> 218 210 </div> 219 211 220 - <Dialog id="add-auth-method-dialog"> 221 - <DialogSurface> 222 - <DialogBody> 223 - <DialogTitle>Add sign-in method</DialogTitle> 212 + <Dialog.Root id="add-auth-method-dialog"> 213 + <Dialog.Surface> 214 + <Dialog.Body> 215 + <Dialog.Title>Add sign-in method</Dialog.Title> 224 216 225 - <DialogContent class="flex flex-col"> 217 + <Dialog.Content class="flex flex-col"> 226 218 <a 227 219 href={routes.account.security.totp.register.href()} 228 220 class="flex items-center gap-4 rounded-md px-4 py-3 text-left outline-2 -outline-offset-2 outline-transparent transition hover:bg-subtle-background-hover focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active" ··· 245 237 246 238 <div class="min-w-0 grow"> 247 239 <p class="text-base-300 font-medium">Security key</p> 248 - <p class="text-base-300 text-neutral-foreground-3"> 249 - Use a hardware key like YubiKey 250 - </p> 240 + <p class="text-base-300 text-neutral-foreground-3">Use a hardware key like YubiKey</p> 251 241 </div> 252 242 </a> 253 243 ··· 261 251 </p> 262 252 </div> 263 253 </button> 264 - </DialogContent> 254 + </Dialog.Content> 265 255 266 - <DialogActions> 267 - <DialogClose> 256 + <Dialog.Actions> 257 + <Dialog.Close> 268 258 <Button>Cancel</Button> 269 - </DialogClose> 270 - </DialogActions> 271 - </DialogBody> 272 - </DialogSurface> 273 - </Dialog> 259 + </Dialog.Close> 260 + </Dialog.Actions> 261 + </Dialog.Body> 262 + </Dialog.Surface> 263 + </Dialog.Root> 274 264 </div> 275 265 ); 276 266 }; ··· 295 285 </div> 296 286 297 287 {backupCodeCount > 0 ? ( 298 - <Menu> 299 - <MenuTrigger> 288 + <Menu.Root> 289 + <Menu.Trigger> 300 290 <button class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-subtle-background text-neutral-foreground-3 outline-2 -outline-offset-2 outline-transparent transition hover:bg-subtle-background-hover hover:text-neutral-foreground-3-hover focus-visible:outline-stroke-focus-2 active:bg-subtle-background-active active:text-neutral-foreground-3-active"> 301 291 <DotGrid1x3HorizontalOutlined size={16} /> 302 292 </button> 303 - </MenuTrigger> 293 + </Menu.Trigger> 304 294 305 - <MenuPopover> 306 - <MenuList> 307 - <MenuItem href={routes.account.security.recovery.show.href()}>View codes</MenuItem> 308 - <MenuItem href={routes.account.security.recovery.regenerate.href()}> 295 + <Menu.Popover> 296 + <Menu.List> 297 + <Menu.Item href={routes.account.security.recovery.show.href()}>View codes</Menu.Item> 298 + <Menu.Item href={routes.account.security.recovery.regenerate.href()}> 309 299 Regenerate codes 310 - </MenuItem> 300 + </Menu.Item> 311 301 312 - <MenuDivider /> 302 + <Menu.Divider /> 313 303 314 - <MenuItem href={routes.account.security.recovery.remove.href()}>Remove all codes</MenuItem> 315 - </MenuList> 316 - </MenuPopover> 317 - </Menu> 304 + <Menu.Item href={routes.account.security.recovery.remove.href()}> 305 + Remove all codes 306 + </Menu.Item> 307 + </Menu.List> 308 + </Menu.Popover> 309 + </Menu.Root> 318 310 ) : ( 319 311 <Button href={routes.account.security.recovery.show.href()}>Generate</Button> 320 312 )}
+22 -26
packages/danaus/src/web/controllers/account/security/recovery.tsx
··· 5 5 import { BaseLayout } from '#web/layouts/base.tsx'; 6 6 import { getAppContext } from '#web/middlewares/app-context.ts'; 7 7 import { getSession } from '#web/middlewares/session.ts'; 8 - import Button from '#web/primitives/button.tsx'; 9 - import DialogActions from '#web/primitives/dialog-actions.tsx'; 10 - import DialogBody from '#web/primitives/dialog-body.tsx'; 11 - import DialogContent from '#web/primitives/dialog-content.tsx'; 12 - import DialogTitle from '#web/primitives/dialog-title.tsx'; 8 + import { Button, Dialog } from '#web/primitives/index.ts'; 13 9 import { routes } from '#web/routes.ts'; 14 10 15 11 import { deleteBackupCodesForm, generateBackupCodesForm } from './recovery/lib/forms'; ··· 43 39 44 40 <div class="flex flex-1 items-center justify-center p-4"> 45 41 <div class="w-full max-w-120 rounded-xl bg-neutral-background-1 shadow-64"> 46 - <DialogBody> 47 - <DialogTitle>Recovery codes</DialogTitle> 42 + <Dialog.Body> 43 + <Dialog.Title>Recovery codes</Dialog.Title> 48 44 49 - <DialogContent class="flex flex-col gap-4"> 45 + <Dialog.Content class="flex flex-col gap-4"> 50 46 <p class="text-base-300"> 51 47 Save these codes in a secure place. Each code can only be used once. 52 48 </p> ··· 58 54 </code> 59 55 ))} 60 56 </div> 61 - </DialogContent> 57 + </Dialog.Content> 62 58 63 - <DialogActions> 59 + <Dialog.Actions> 64 60 <Button href={routes.account.security.overview.href()}>Done</Button> 65 - </DialogActions> 66 - </DialogBody> 61 + </Dialog.Actions> 62 + </Dialog.Body> 67 63 </div> 68 64 </div> 69 65 </BaseLayout>, ··· 95 91 <div class="flex flex-1 items-center justify-center p-4"> 96 92 <div class="w-full max-w-120 rounded-xl bg-neutral-background-1 shadow-64"> 97 93 <form {...generateBackupCodesForm} class="contents"> 98 - <DialogBody> 99 - <DialogTitle>Regenerate recovery codes?</DialogTitle> 94 + <Dialog.Body> 95 + <Dialog.Title>Regenerate recovery codes?</Dialog.Title> 100 96 101 - <DialogContent> 97 + <Dialog.Content> 102 98 <p> 103 99 This will invalidate your existing recovery codes. Make sure to save the new ones. 104 100 </p> ··· 108 104 {error.message} 109 105 </p> 110 106 )} 111 - </DialogContent> 107 + </Dialog.Content> 112 108 113 - <DialogActions> 109 + <Dialog.Actions> 114 110 <Button type="button" href={routes.account.security.overview.href()}> 115 111 Cancel 116 112 </Button> ··· 118 114 <Button type="submit" variant="primary"> 119 115 Regenerate 120 116 </Button> 121 - </DialogActions> 122 - </DialogBody> 117 + </Dialog.Actions> 118 + </Dialog.Body> 123 119 </form> 124 120 </div> 125 121 </div> ··· 153 149 <div class="flex flex-1 items-center justify-center p-4"> 154 150 <div class="w-full max-w-120 rounded-xl bg-neutral-background-1 shadow-64"> 155 151 <form {...deleteBackupCodesForm} class="contents"> 156 - <DialogBody> 157 - <DialogTitle>Delete recovery codes?</DialogTitle> 152 + <Dialog.Body> 153 + <Dialog.Title>Delete recovery codes?</Dialog.Title> 158 154 159 - <DialogContent> 155 + <Dialog.Content> 160 156 <p class="text-base-300"> 161 157 You won't be able to use recovery codes to sign in until you generate new ones. 162 158 </p> ··· 166 162 {error.message} 167 163 </p> 168 164 )} 169 - </DialogContent> 165 + </Dialog.Content> 170 166 171 - <DialogActions> 167 + <Dialog.Actions> 172 168 <Button type="button" href={routes.account.security.overview.href()}> 173 169 Cancel 174 170 </Button> ··· 176 172 <Button type="submit" variant="primary"> 177 173 Delete 178 174 </Button> 179 - </DialogActions> 180 - </DialogBody> 175 + </Dialog.Actions> 176 + </Dialog.Body> 181 177 </form> 182 178 </div> 183 179 </div>
+15 -21
packages/danaus/src/web/controllers/account/security/totp.tsx
··· 14 14 import { BaseLayout } from '#web/layouts/base.tsx'; 15 15 import { getAppContext } from '#web/middlewares/app-context.ts'; 16 16 import { getSession } from '#web/middlewares/session.ts'; 17 - import Button from '#web/primitives/button.tsx'; 18 - import DialogActions from '#web/primitives/dialog-actions.tsx'; 19 - import DialogBody from '#web/primitives/dialog-body.tsx'; 20 - import DialogContent from '#web/primitives/dialog-content.tsx'; 21 - import DialogTitle from '#web/primitives/dialog-title.tsx'; 22 - import Field from '#web/primitives/field.tsx'; 23 - import Input from '#web/primitives/input.tsx'; 17 + import { Button, Dialog, Field, Input } from '#web/primitives/index.ts'; 24 18 import { routes } from '#web/routes.ts'; 25 19 26 20 import { removeTotpForm, setupTotpForm } from './totp/lib/forms'; ··· 67 61 <div class="flex flex-1 items-center justify-center p-4"> 68 62 <div class="w-full max-w-150 rounded-xl bg-neutral-background-1 shadow-64"> 69 63 <form {...setupTotpForm} class="contents"> 70 - <DialogBody> 71 - <DialogTitle>Set up authenticator app</DialogTitle> 64 + <Dialog.Body> 65 + <Dialog.Title>Set up authenticator app</Dialog.Title> 72 66 73 - <DialogContent class="flex flex-col gap-4"> 67 + <Dialog.Content class="flex flex-col gap-4"> 74 68 <p class="text-base-300"> 75 69 Scan this QR code with your authenticator app, or enter the code manually. 76 70 </p> ··· 136 130 {generalError.message} 137 131 </p> 138 132 )} 139 - </DialogContent> 133 + </Dialog.Content> 140 134 141 - <DialogActions> 135 + <Dialog.Actions> 142 136 <Button type="button" href={routes.account.security.overview.href()}> 143 137 Cancel 144 138 </Button> ··· 146 140 <Button type="submit" variant="primary"> 147 141 Save 148 142 </Button> 149 - </DialogActions> 150 - </DialogBody> 143 + </Dialog.Actions> 144 + </Dialog.Body> 151 145 </form> 152 146 </div> 153 147 </div> ··· 189 183 <form {...removeTotpForm} class="contents"> 190 184 <input {...fields.id.as('hidden', params.id)} /> 191 185 192 - <DialogBody> 193 - <DialogTitle>Remove this authenticator?</DialogTitle> 186 + <Dialog.Body> 187 + <Dialog.Title>Remove this authenticator?</Dialog.Title> 194 188 195 - <DialogContent> 189 + <Dialog.Content> 196 190 <p class="text-base-300">You'll no longer be able to use "{totp.name}" to sign in.</p> 197 191 198 192 {error && ( ··· 200 194 {error.message} 201 195 </p> 202 196 )} 203 - </DialogContent> 197 + </Dialog.Content> 204 198 205 - <DialogActions> 199 + <Dialog.Actions> 206 200 <Button type="button" href={routes.account.security.overview.href()}> 207 201 Cancel 208 202 </Button> ··· 210 204 <Button type="submit" variant="primary"> 211 205 Remove 212 206 </Button> 213 - </DialogActions> 214 - </DialogBody> 207 + </Dialog.Actions> 208 + </Dialog.Body> 215 209 </form> 216 210 </div> 217 211 </div>
+19 -40
packages/danaus/src/web/controllers/account/security/webauthn.tsx
··· 8 8 import { BaseLayout } from '#web/layouts/base.tsx'; 9 9 import { getAppContext } from '#web/middlewares/app-context.ts'; 10 10 import { getSession } from '#web/middlewares/session.ts'; 11 - import Button from '#web/primitives/button.tsx'; 12 - import DialogActions from '#web/primitives/dialog-actions.tsx'; 13 - import DialogBody from '#web/primitives/dialog-body.tsx'; 14 - import DialogContent from '#web/primitives/dialog-content.tsx'; 15 - import DialogTitle from '#web/primitives/dialog-title.tsx'; 16 - import Field from '#web/primitives/field.tsx'; 17 - import Input from '#web/primitives/input.tsx'; 11 + import { Button, Dialog, Field, Input } from '#web/primitives/index.ts'; 18 12 import { routes } from '#web/routes.ts'; 19 13 20 - import { 21 - completeWebAuthnForm, 22 - initiateWebAuthnRegistration, 23 - removeWebAuthnForm, 24 - } from './webauthn/lib/forms'; 14 + import { completeWebAuthnForm, initiateWebAuthnRegistration, removeWebAuthnForm } from './webauthn/lib/forms'; 25 15 26 16 export default { 27 17 middleware: [], ··· 50 40 const existingChallenge = accountManager.getWebAuthnChallenge(token); 51 41 if (existingChallenge) { 52 42 // regenerate options with the same challenge 53 - const state = await initiateWebAuthnRegistration( 54 - session.did, 55 - account.handle ?? session.did, 56 - ); 43 + const state = await initiateWebAuthnRegistration(session.did, account.handle ?? session.did); 57 44 // delete old challenge and use new one 58 45 accountManager.deleteWebAuthnChallenge(token); 59 46 token = state.token; ··· 63 50 64 51 if (!options) { 65 52 // generate new registration 66 - const state = await initiateWebAuthnRegistration( 67 - session.did, 68 - account.handle ?? session.did, 69 - ); 53 + const state = await initiateWebAuthnRegistration(session.did, account.handle ?? session.did); 70 54 token = state.token; 71 55 options = state.options; 72 56 } ··· 82 66 <div class="flex flex-1 items-center justify-center p-4"> 83 67 <div class="w-full max-w-120 rounded-xl bg-neutral-background-1 shadow-64"> 84 68 <form {...completeWebAuthnForm} class="contents"> 85 - <DialogBody> 86 - <DialogTitle>Set up security key</DialogTitle> 69 + <Dialog.Body> 70 + <Dialog.Title>Set up security key</Dialog.Title> 87 71 88 - <DialogContent class="flex flex-col gap-4"> 72 + <Dialog.Content class="flex flex-col gap-4"> 89 73 <p class="text-base-300"> 90 74 Insert your security key and follow your browser's prompts to register it. 91 75 </p> ··· 125 109 {generalError.message} 126 110 </p> 127 111 )} 128 - </DialogContent> 112 + </Dialog.Content> 129 113 130 - <DialogActions> 114 + <Dialog.Actions> 131 115 <Button type="button" href={routes.account.security.overview.href()}> 132 116 Cancel 133 117 </Button> 134 118 135 - <Button 136 - type="submit" 137 - variant="primary" 138 - disabled 139 - data-target="webauthn-register.submit" 140 - > 119 + <Button type="submit" variant="primary" disabled data-target="webauthn-register.submit"> 141 120 Save 142 121 </Button> 143 - </DialogActions> 144 - </DialogBody> 122 + </Dialog.Actions> 123 + </Dialog.Body> 145 124 </form> 146 125 </div> 147 126 </div> ··· 183 162 <form {...removeWebAuthnForm} class="contents"> 184 163 <input {...fields.id.as('hidden', params.id)} /> 185 164 186 - <DialogBody> 187 - <DialogTitle>Remove this security key?</DialogTitle> 165 + <Dialog.Body> 166 + <Dialog.Title>Remove this security key?</Dialog.Title> 188 167 189 - <DialogContent> 168 + <Dialog.Content> 190 169 <p class="text-base-300"> 191 170 You'll no longer be able to use "{credential.name}" to sign in. 192 171 </p> ··· 196 175 {error.message} 197 176 </p> 198 177 )} 199 - </DialogContent> 178 + </Dialog.Content> 200 179 201 - <DialogActions> 180 + <Dialog.Actions> 202 181 <Button type="button" href={routes.account.security.overview.href()}> 203 182 Cancel 204 183 </Button> ··· 206 185 <Button type="submit" variant="primary"> 207 186 Remove 208 187 </Button> 209 - </DialogActions> 210 - </DialogBody> 188 + </Dialog.Actions> 189 + </Dialog.Body> 211 190 </form> 212 191 </div> 213 192 </div>
+9 -12
packages/danaus/src/web/controllers/admin.tsx
··· 2 2 import { forms } from '@oomfware/forms'; 3 3 import { render } from '@oomfware/jsx'; 4 4 5 - import StatCard from '../admin/components/stat-card.tsx'; 6 - import { createAccountForm } from '../admin/forms.ts'; 7 - import MagnifyingGlassOutlined from '../icons/central/magnifying-glass-outlined.tsx'; 8 - import PlusLargeOutlined from '../icons/central/plus-large-outlined.tsx'; 9 - import { AdminLayout } from '../layouts/admin.tsx'; 10 - import { getAppContext } from '../middlewares/app-context.ts'; 11 - import { requireAdmin } from '../middlewares/basic-auth.ts'; 12 - import Button from '../primitives/button.tsx'; 13 - import Field from '../primitives/field.tsx'; 14 - import Input from '../primitives/input.tsx'; 15 - import Select from '../primitives/select.tsx'; 16 - import { routes } from '../routes.ts'; 5 + import StatCard from '#web/admin/components/stat-card.tsx'; 6 + import { createAccountForm } from '#web/admin/forms.ts'; 7 + import MagnifyingGlassOutlined from '#web/icons/central/magnifying-glass-outlined.tsx'; 8 + import PlusLargeOutlined from '#web/icons/central/plus-large-outlined.tsx'; 9 + import { AdminLayout } from '#web/layouts/admin.tsx'; 10 + import { getAppContext } from '#web/middlewares/app-context.ts'; 11 + import { requireAdmin } from '#web/middlewares/basic-auth.ts'; 12 + import { Button, Field, Input, Select } from '#web/primitives/index.ts'; 13 + import { routes } from '#web/routes.ts'; 17 14 18 15 export default { 19 16 middleware: [requireAdmin(), forms({ createAccountForm })],
+1 -4
packages/danaus/src/web/controllers/login.tsx
··· 3 3 import { render } from '@oomfware/jsx'; 4 4 5 5 import { BaseLayout } from '#web/layouts/base.tsx'; 6 - import Button from '#web/primitives/button.tsx'; 7 - import Checkbox from '#web/primitives/checkbox.tsx'; 8 - import Field from '#web/primitives/field.tsx'; 9 - import Input from '#web/primitives/input.tsx'; 6 + import { Button, Checkbox, Field, Input } from '#web/primitives/index.ts'; 10 7 import { routes } from '#web/routes.ts'; 11 8 12 9 import { loginForm } from './login/lib/forms.ts';
+4 -4
packages/danaus/src/web/controllers/oauth.tsx
··· 1 1 import type { Controller } from '@oomfware/fetch-router'; 2 2 import { render } from '@oomfware/jsx'; 3 3 4 - import { BaseLayout } from '../layouts/base.tsx'; 5 - import { requireSession } from '../middlewares/session.ts'; 6 - import Button from '../primitives/button.tsx'; 7 - import { routes } from '../routes.ts'; 4 + import { BaseLayout } from '#web/layouts/base.tsx'; 5 + import { requireSession } from '#web/middlewares/session.ts'; 6 + import { Button } from '#web/primitives/index.ts'; 7 + import { routes } from '#web/routes.ts'; 8 8 9 9 export default { 10 10 authorize: {
+16 -25
packages/danaus/src/web/controllers/verify.tsx
··· 15 15 import { BaseLayout } from '#web/layouts/base.tsx'; 16 16 import { getAppContext } from '#web/middlewares/app-context.ts'; 17 17 import { tryGetSession } from '#web/middlewares/session.ts'; 18 - import Button from '#web/primitives/button.tsx'; 19 - import Field from '#web/primitives/field.tsx'; 20 - import Input from '#web/primitives/input.tsx'; 21 - import MenuItem from '#web/primitives/menu-item.tsx'; 22 - import MenuList from '#web/primitives/menu-list.tsx'; 23 - import MenuPopover from '#web/primitives/menu-popover.tsx'; 24 - import MenuTrigger from '#web/primitives/menu-trigger.tsx'; 25 - import Menu from '#web/primitives/menu.tsx'; 18 + import { Button, Field, Input, Menu } from '#web/primitives/index.ts'; 26 19 import { routes } from '#web/routes.ts'; 27 20 28 21 import { verifyForm, verifyWebAuthnForm, type AuthFactor } from './login/lib/forms.ts'; ··· 115 108 const tokenParam = ctx.isSudo ? undefined : ctx.challenge.token; 116 109 switch (ctx.mfaStatus.preferred) { 117 110 case PreferredMfa.WebAuthn: { 118 - redirect( 119 - routes.verify.webauthn.href(undefined, { token: tokenParam, redirect: ctx.redirectUrl }), 120 - ); 111 + redirect(routes.verify.webauthn.href(undefined, { token: tokenParam, redirect: ctx.redirectUrl })); 121 112 } 122 113 case PreferredMfa.Totp: { 123 114 redirect(routes.verify.totp.href(undefined, { token: tokenParam, redirect: ctx.redirectUrl })); ··· 402 393 const tokenParam = isSudo ? undefined : challenge; 403 394 404 395 return ( 405 - <Menu> 406 - <MenuTrigger> 396 + <Menu.Root> 397 + <Menu.Trigger> 407 398 <Button>Show other methods</Button> 408 - </MenuTrigger> 399 + </Menu.Trigger> 409 400 410 - <MenuPopover> 411 - <MenuList> 401 + <Menu.Popover> 402 + <Menu.List> 412 403 {props.factor !== 'webauthn' && mfaStatus.hasWebAuthn && ( 413 - <MenuItem 404 + <Menu.Item 414 405 href={routes.verify.webauthn.href(undefined, { 415 406 token: tokenParam, 416 407 redirect: redirectUrl, 417 408 })} 418 409 > 419 410 Use security key 420 - </MenuItem> 411 + </Menu.Item> 421 412 )} 422 413 423 414 {props.factor !== 'totp' && mfaStatus.hasTotp && ( 424 - <MenuItem 415 + <Menu.Item 425 416 href={routes.verify.totp.href(undefined, { 426 417 token: tokenParam, 427 418 redirect: redirectUrl, 428 419 })} 429 420 > 430 421 Use authenticator app 431 - </MenuItem> 422 + </Menu.Item> 432 423 )} 433 424 434 425 {props.factor !== 'recovery' && mfaStatus.hasRecoveryCodes && ( 435 - <MenuItem 426 + <Menu.Item 436 427 href={routes.verify.recovery.href(undefined, { 437 428 token: tokenParam, 438 429 redirect: redirectUrl, 439 430 })} 440 431 > 441 432 Use 2FA recovery code 442 - </MenuItem> 433 + </Menu.Item> 443 434 )} 444 - </MenuList> 445 - </MenuPopover> 446 - </Menu> 435 + </Menu.List> 436 + </Menu.Popover> 437 + </Menu.Root> 447 438 ); 448 439 };
+1 -1
packages/danaus/src/web/primitives/accordion-header.tsx packages/danaus/src/web/primitives/accordion/header.tsx
··· 2 2 3 3 import { cva } from 'cva'; 4 4 5 - import ChevronDownSmallOutlined from '../icons/central/chevron-down-small-outlined.tsx'; 5 + import ChevronDownSmallOutlined from '../../icons/central/chevron-down-small-outlined.tsx'; 6 6 7 7 const root = cva({ 8 8 base: [
+7 -1
packages/danaus/src/web/primitives/accordion-item.tsx packages/danaus/src/web/primitives/accordion/item.tsx
··· 1 1 import type { JSXNode } from '@oomfware/jsx'; 2 2 3 + import { cva } from 'cva'; 4 + 3 5 export interface AccordionItemProps { 4 6 /** whether the accordion item is open by default */ 5 7 open?: boolean; ··· 9 11 children?: JSXNode; 10 12 } 11 13 14 + const root = cva({ 15 + base: 'group/accordion-item', 16 + }); 17 + 12 18 /** 13 19 * accordion item component using native `<details>` element 14 20 */ ··· 16 22 const { open = false, name, class: className, children } = props; 17 23 18 24 return ( 19 - <details open={open} name={name} class={`group/accordion-item${className ? ` ${className}` : ''}`}> 25 + <details open={open} name={name} class={root({ className })}> 20 26 {children} 21 27 </details> 22 28 );
packages/danaus/src/web/primitives/accordion-panel.tsx packages/danaus/src/web/primitives/accordion/panel.tsx
+8 -16
packages/danaus/src/web/primitives/accordion.tsx
··· 1 - import type { JSXNode } from '@oomfware/jsx'; 2 - 3 - export interface AccordionProps { 4 - class?: string; 5 - children?: JSXNode; 6 - } 7 - 8 - /** 9 - * accordion container component 10 - */ 11 - const Accordion = (props: AccordionProps) => { 12 - const { class: className, children } = props; 13 - 14 - return <div class={className}>{children}</div>; 15 - }; 1 + export { default as Root } from './accordion/root.tsx'; 2 + export { default as Item } from './accordion/item.tsx'; 3 + export { default as Header } from './accordion/header.tsx'; 4 + export { default as Panel } from './accordion/panel.tsx'; 16 5 17 - export default Accordion; 6 + export type { AccordionProps } from './accordion/root.tsx'; 7 + export type { AccordionItemProps } from './accordion/item.tsx'; 8 + export type { AccordionHeaderProps } from './accordion/header.tsx'; 9 + export type { AccordionPanelProps } from './accordion/panel.tsx';
+17
packages/danaus/src/web/primitives/accordion/root.tsx
··· 1 + import type { JSXNode } from '@oomfware/jsx'; 2 + 3 + export interface AccordionProps { 4 + class?: string; 5 + children?: JSXNode; 6 + } 7 + 8 + /** 9 + * accordion container component 10 + */ 11 + const Accordion = (props: AccordionProps) => { 12 + const { class: className, children } = props; 13 + 14 + return <div class={className}>{children}</div>; 15 + }; 16 + 17 + export default Accordion;
packages/danaus/src/web/primitives/dialog-actions.tsx packages/danaus/src/web/primitives/dialog/actions.tsx
packages/danaus/src/web/primitives/dialog-body.tsx packages/danaus/src/web/primitives/dialog/body.tsx
+1 -1
packages/danaus/src/web/primitives/dialog-close.tsx packages/danaus/src/web/primitives/dialog/close.tsx
··· 1 1 import { cloneElement, type JSXElement } from '@oomfware/jsx'; 2 2 3 - import { useDialogContext } from './utils/dialog-context.tsx'; 3 + import { useDialogContext } from './utils/context.tsx'; 4 4 5 5 export interface DialogCloseProps { 6 6 children: JSXElement;
packages/danaus/src/web/primitives/dialog-content.tsx packages/danaus/src/web/primitives/dialog/content.tsx
+1 -1
packages/danaus/src/web/primitives/dialog-surface.tsx packages/danaus/src/web/primitives/dialog/surface.tsx
··· 2 2 3 3 import { cva, type VariantProps } from 'cva'; 4 4 5 - import { useDialogContext } from './utils/dialog-context'; 5 + import { useDialogContext } from './utils/context.tsx'; 6 6 7 7 const root = cva({ 8 8 base: [
+1 -1
packages/danaus/src/web/primitives/dialog-title.tsx packages/danaus/src/web/primitives/dialog/title.tsx
··· 2 2 3 3 import { cva } from 'cva'; 4 4 5 - import { useDialogContext } from './utils/dialog-context.tsx'; 5 + import { useDialogContext } from './utils/context.tsx'; 6 6 7 7 const root = cva({ 8 8 base: ['m-0 flex items-start gap-2', 'text-base-500 font-semibold'],
+1 -1
packages/danaus/src/web/primitives/dialog-trigger.tsx packages/danaus/src/web/primitives/dialog/trigger.tsx
··· 1 1 import { cloneElement, type JSXElement } from '@oomfware/jsx'; 2 2 3 - import { useDialogContext } from './utils/dialog-context.tsx'; 3 + import { useDialogContext } from './utils/context.tsx'; 4 4 5 5 export interface DialogTriggerProps { 6 6 children: JSXElement;
+16 -27
packages/danaus/src/web/primitives/dialog.tsx
··· 1 - import type { JSXNode } from '@oomfware/jsx'; 2 - 3 - import { useId } from '../components/id.tsx'; 4 - 5 - import { DialogContext, type DialogContextValue } from './utils/dialog-context.tsx'; 1 + export { default as Root } from './dialog/root.tsx'; 2 + export { default as Trigger } from './dialog/trigger.tsx'; 3 + export { default as Close } from './dialog/close.tsx'; 4 + export { default as Actions } from './dialog/actions.tsx'; 5 + export { default as Content } from './dialog/content.tsx'; 6 + export { default as Title } from './dialog/title.tsx'; 7 + export { default as Surface } from './dialog/surface.tsx'; 8 + export { default as Body } from './dialog/body.tsx'; 6 9 7 - export interface DialogProps { 8 - id?: string; 9 - children?: JSXNode; 10 - } 11 - 12 - /** 13 - * dialog root component wrapping native `<dialog>` element 14 - * @param props.id unique identifier for invoker command targeting 15 - * @param props.class additional CSS classes 16 - */ 17 - const Dialog = (props: DialogProps) => { 18 - const { id = useId(), children } = props; 19 - 20 - const contextValue: DialogContextValue = { 21 - dialogId: id, 22 - titleId: useId(), 23 - }; 24 - 25 - return <DialogContext.Provider value={contextValue}>{children}</DialogContext.Provider>; 26 - }; 27 - 28 - export default Dialog; 10 + export type { DialogProps } from './dialog/root.tsx'; 11 + export type { DialogTriggerProps } from './dialog/trigger.tsx'; 12 + export type { DialogCloseProps } from './dialog/close.tsx'; 13 + export type { DialogActionsProps } from './dialog/actions.tsx'; 14 + export type { DialogContentProps } from './dialog/content.tsx'; 15 + export type { DialogTitleProps } from './dialog/title.tsx'; 16 + export type { DialogSurfaceProps } from './dialog/surface.tsx'; 17 + export type { DialogBodyProps } from './dialog/body.tsx';
+28
packages/danaus/src/web/primitives/dialog/root.tsx
··· 1 + import type { JSXNode } from '@oomfware/jsx'; 2 + 3 + import { useId } from '../../components/id.tsx'; 4 + 5 + import { DialogContext, type DialogContextValue } from './utils/context.tsx'; 6 + 7 + export interface DialogProps { 8 + id?: string; 9 + children?: JSXNode; 10 + } 11 + 12 + /** 13 + * dialog root component wrapping native `<dialog>` element 14 + * @param props.id unique identifier for invoker command targeting 15 + * @param props.class additional CSS classes 16 + */ 17 + const Dialog = (props: DialogProps) => { 18 + const { id = useId(), children } = props; 19 + 20 + const contextValue: DialogContextValue = { 21 + dialogId: id, 22 + titleId: useId(), 23 + }; 24 + 25 + return <DialogContext.Provider value={contextValue}>{children}</DialogContext.Provider>; 26 + }; 27 + 28 + export default Dialog;
+25
packages/danaus/src/web/primitives/index.ts
··· 1 + // namespace exports for compound components 2 + export * as Dialog from './dialog.tsx'; 3 + export * as Accordion from './accordion.tsx'; 4 + export * as MessageBar from './message-bar.tsx'; 5 + export * as Menu from './menu.tsx'; 6 + 7 + // direct exports for simple components 8 + export { default as Button } from './button.tsx'; 9 + export { default as Checkbox } from './checkbox.tsx'; 10 + export { default as Field } from './field.tsx'; 11 + export { default as Input } from './input.tsx'; 12 + export { default as Label } from './label.tsx'; 13 + export { default as Radio } from './radio.tsx'; 14 + export { default as RadioGroup } from './radio-group.tsx'; 15 + export { default as Select } from './select.tsx'; 16 + 17 + // type re-exports 18 + export type { ButtonProps } from './button.tsx'; 19 + export type { CheckboxProps } from './checkbox.tsx'; 20 + export type { FieldProps } from './field.tsx'; 21 + export type { InputProps } from './input.tsx'; 22 + export type { LabelProps } from './label.tsx'; 23 + export type { RadioProps } from './radio.tsx'; 24 + export type { RadioGroupProps } from './radio-group.tsx'; 25 + export type { SelectProps } from './select.tsx';
packages/danaus/src/web/primitives/menu-divider.tsx packages/danaus/src/web/primitives/menu/divider.tsx
+1 -1
packages/danaus/src/web/primitives/menu-item.tsx packages/danaus/src/web/primitives/menu/item.tsx
··· 2 2 3 3 import { cva } from 'cva'; 4 4 5 - import type { InvokerCommand } from './utils/types.ts'; 5 + import type { InvokerCommand } from '../utils/types.ts'; 6 6 7 7 const root = cva({ 8 8 base: [
packages/danaus/src/web/primitives/menu-list.tsx packages/danaus/src/web/primitives/menu/list.tsx
+1 -1
packages/danaus/src/web/primitives/menu-popover.tsx packages/danaus/src/web/primitives/menu/popover.tsx
··· 2 2 3 3 import { cva } from 'cva'; 4 4 5 - import { useMenuContext } from './utils/menu-context.tsx'; 5 + import { useMenuContext } from './utils/context.tsx'; 6 6 7 7 const root = cva({ 8 8 base: [
+1 -1
packages/danaus/src/web/primitives/menu-trigger.tsx packages/danaus/src/web/primitives/menu/trigger.tsx
··· 2 2 3 3 import { cx } from 'cva'; 4 4 5 - import { useMenuContext } from './utils/menu-context.tsx'; 5 + import { useMenuContext } from './utils/context.tsx'; 6 6 7 7 export interface MenuTriggerProps { 8 8 children: JSXElement;
+12 -32
packages/danaus/src/web/primitives/menu.tsx
··· 1 - import type { JSXNode } from '@oomfware/jsx'; 2 - 3 - import { useId } from '../components/id.tsx'; 4 - 5 - import { MenuContext, type MenuContextValue } from './utils/menu-context.tsx'; 6 - 7 - export interface MenuProps { 8 - id?: string; 9 - children?: JSXNode; 10 - } 11 - 12 - /** 13 - * menu context provider for coordinating trigger and popover, 14 - * sets the --anchor CSS variable for anchor positioning 15 - * @param props.id unique identifier for invoker command targeting 16 - */ 17 - const Menu = (props: MenuProps) => { 18 - const { id = useId(), children } = props; 19 - 20 - const contextValue: MenuContextValue = { 21 - menuId: id, 22 - }; 23 - 24 - return ( 25 - <MenuContext.Provider value={contextValue}> 26 - <div class="contents" style={{ '--anchor': `--menu-${id}` }}> 27 - {children} 28 - </div> 29 - </MenuContext.Provider> 30 - ); 31 - }; 1 + export { default as Root } from './menu/root.tsx'; 2 + export { default as Trigger } from './menu/trigger.tsx'; 3 + export { default as List } from './menu/list.tsx'; 4 + export { default as Item } from './menu/item.tsx'; 5 + export { default as Divider } from './menu/divider.tsx'; 6 + export { default as Popover } from './menu/popover.tsx'; 32 7 33 - export default Menu; 8 + export type { MenuProps } from './menu/root.tsx'; 9 + export type { MenuTriggerProps } from './menu/trigger.tsx'; 10 + export type { MenuListProps } from './menu/list.tsx'; 11 + export type { MenuItemProps } from './menu/item.tsx'; 12 + export type { MenuDividerProps } from './menu/divider.tsx'; 13 + export type { MenuPopoverProps } from './menu/popover.tsx';
+33
packages/danaus/src/web/primitives/menu/root.tsx
··· 1 + import type { JSXNode } from '@oomfware/jsx'; 2 + 3 + import { useId } from '../../components/id.tsx'; 4 + 5 + import { MenuContext, type MenuContextValue } from './utils/context.tsx'; 6 + 7 + export interface MenuProps { 8 + id?: string; 9 + children?: JSXNode; 10 + } 11 + 12 + /** 13 + * menu context provider for coordinating trigger and popover, 14 + * sets the --anchor CSS variable for anchor positioning 15 + * @param props.id unique identifier for invoker command targeting 16 + */ 17 + const Menu = (props: MenuProps) => { 18 + const { id = useId(), children } = props; 19 + 20 + const contextValue: MenuContextValue = { 21 + menuId: id, 22 + }; 23 + 24 + return ( 25 + <MenuContext.Provider value={contextValue}> 26 + <div class="contents" style={{ '--anchor': `--menu-${id}` }}> 27 + {children} 28 + </div> 29 + </MenuContext.Provider> 30 + ); 31 + }; 32 + 33 + export default Menu;
+1 -1
packages/danaus/src/web/primitives/message-bar-actions.tsx packages/danaus/src/web/primitives/message-bar/actions.tsx
··· 2 2 3 3 import { cva } from 'cva'; 4 4 5 - import { useMessageBarContext } from './utils/message-bar-context.tsx'; 5 + import { useMessageBarContext } from './utils/context.tsx'; 6 6 7 7 const root = cva({ 8 8 base: ['flex items-center gap-3 pr-3', '[grid-area:secondaryActions]'],
packages/danaus/src/web/primitives/message-bar-body.tsx packages/danaus/src/web/primitives/message-bar/body.tsx
packages/danaus/src/web/primitives/message-bar-title.tsx packages/danaus/src/web/primitives/message-bar/title.tsx
+8 -103
packages/danaus/src/web/primitives/message-bar.tsx
··· 1 - import type { JSXNode } from '@oomfware/jsx'; 2 - 3 - import { cva } from 'cva'; 4 - 5 - import CheckCircle2Solid from '../icons/central/check-circle-2-solid.tsx'; 6 - import CircleInfoSolid from '../icons/central/circle-info-solid.tsx'; 7 - import CircleXSolid from '../icons/central/circle-x-solid.tsx'; 8 - import ExclamationTriangleSolid from '../icons/central/exclamation-triangle-solid.tsx'; 9 - 10 - import { 11 - MessageBarContext, 12 - type MessageBarContextValue, 13 - type MessageBarIntent, 14 - type MessageBarLayout, 15 - } from './utils/message-bar-context.tsx'; 16 - 17 - const getIntentIcon = (intent: MessageBarIntent): JSXNode => { 18 - switch (intent) { 19 - case 'info': 20 - return <CircleInfoSolid size={20} />; 21 - case 'warning': 22 - return <ExclamationTriangleSolid size={20} />; 23 - case 'error': 24 - return <CircleXSolid size={20} />; 25 - case 'success': 26 - return <CheckCircle2Solid size={20} />; 27 - } 28 - }; 1 + export { default as Root } from './message-bar/root.tsx'; 2 + export { default as Body } from './message-bar/body.tsx'; 3 + export { default as Title } from './message-bar/title.tsx'; 4 + export { default as Actions } from './message-bar/actions.tsx'; 29 5 30 - const root = cva({ 31 - base: ['grid', 'min-h-9', 'rounded-md border pl-3'], 32 - variants: { 33 - intent: { 34 - info: 'border-neutral-stroke-1 bg-neutral-background-3', 35 - success: 'border-status-success-border-1 bg-status-success-background-1', 36 - warning: 'border-status-warning-border-1 bg-status-warning-background-1', 37 - error: 'border-status-danger-border-1 bg-status-danger-background-1', 38 - }, 39 - layout: { 40 - singleline: [ 41 - 'items-center', 42 - 'grid-cols-[auto_1fr_auto_auto]', 43 - '[grid-template-areas:"icon_body_secondaryActions_actions"]', 44 - ], 45 - multiline: [ 46 - 'items-start py-2', 47 - 'grid-cols-[auto_1fr_auto]', 48 - '[grid-template-areas:"icon_body_actions"_"secondaryActions_secondaryActions_secondaryActions"]', 49 - ], 50 - }, 51 - }, 52 - }); 53 - 54 - const iconStyle = cva({ 55 - base: ['mr-2', 'flex items-center', 'text-base-500'], 56 - variants: { 57 - intent: { 58 - info: 'text-neutral-foreground-3', 59 - success: 'text-status-success-foreground-1', 60 - warning: 'text-status-warning-foreground-3', 61 - error: 'text-status-danger-foreground-1', 62 - }, 63 - }, 64 - }); 65 - 66 - export interface MessageBarProps { 67 - /** 68 - * intent of the message bar 69 - * @default 'info' 70 - */ 71 - intent?: MessageBarIntent; 72 - /** layout of the message bar */ 73 - layout: MessageBarLayout; 74 - /** optional icon to display */ 75 - icon?: JSXNode; 76 - class?: string; 77 - children?: JSXNode; 78 - } 79 - 80 - /** 81 - * message bar component for displaying inline messages 82 - * @param props.intent the intent/severity of the message 83 - * @param props.layout the layout of the message bar 84 - * @param props.icon optional icon to display 85 - * @param props.class additional CSS classes 86 - */ 87 - const MessageBar = (props: MessageBarProps) => { 88 - const { intent = 'info', layout, icon, class: className, children } = props; 89 - 90 - const contextValue: MessageBarContextValue = { intent, layout }; 91 - const renderedIcon = icon ?? getIntentIcon(intent); 92 - 93 - return ( 94 - <MessageBarContext.Provider value={contextValue}> 95 - <div role="group" aria-live="polite" class={root({ intent, layout, className })}> 96 - <span class={iconStyle({ intent })}>{renderedIcon}</span> 97 - 98 - {children} 99 - </div> 100 - </MessageBarContext.Provider> 101 - ); 102 - }; 103 - 104 - export default MessageBar; 6 + export type { MessageBarProps } from './message-bar/root.tsx'; 7 + export type { MessageBarBodyProps } from './message-bar/body.tsx'; 8 + export type { MessageBarTitleProps } from './message-bar/title.tsx'; 9 + export type { MessageBarActionsProps } from './message-bar/actions.tsx';
+104
packages/danaus/src/web/primitives/message-bar/root.tsx
··· 1 + import type { JSXNode } from '@oomfware/jsx'; 2 + 3 + import { cva } from 'cva'; 4 + 5 + import CheckCircle2Solid from '../../icons/central/check-circle-2-solid.tsx'; 6 + import CircleInfoSolid from '../../icons/central/circle-info-solid.tsx'; 7 + import CircleXSolid from '../../icons/central/circle-x-solid.tsx'; 8 + import ExclamationTriangleSolid from '../../icons/central/exclamation-triangle-solid.tsx'; 9 + 10 + import { 11 + MessageBarContext, 12 + type MessageBarContextValue, 13 + type MessageBarIntent, 14 + type MessageBarLayout, 15 + } from './utils/context.tsx'; 16 + 17 + const getIntentIcon = (intent: MessageBarIntent): JSXNode => { 18 + switch (intent) { 19 + case 'info': 20 + return <CircleInfoSolid size={20} />; 21 + case 'warning': 22 + return <ExclamationTriangleSolid size={20} />; 23 + case 'error': 24 + return <CircleXSolid size={20} />; 25 + case 'success': 26 + return <CheckCircle2Solid size={20} />; 27 + } 28 + }; 29 + 30 + const root = cva({ 31 + base: ['grid', 'min-h-9', 'rounded-md border pl-3'], 32 + variants: { 33 + intent: { 34 + info: 'border-neutral-stroke-1 bg-neutral-background-3', 35 + success: 'border-status-success-border-1 bg-status-success-background-1', 36 + warning: 'border-status-warning-border-1 bg-status-warning-background-1', 37 + error: 'border-status-danger-border-1 bg-status-danger-background-1', 38 + }, 39 + layout: { 40 + singleline: [ 41 + 'items-center', 42 + 'grid-cols-[auto_1fr_auto_auto]', 43 + '[grid-template-areas:"icon_body_secondaryActions_actions"]', 44 + ], 45 + multiline: [ 46 + 'items-start py-2', 47 + 'grid-cols-[auto_1fr_auto]', 48 + '[grid-template-areas:"icon_body_actions"_"secondaryActions_secondaryActions_secondaryActions"]', 49 + ], 50 + }, 51 + }, 52 + }); 53 + 54 + const iconStyle = cva({ 55 + base: ['mr-2', 'flex items-center', 'text-base-500'], 56 + variants: { 57 + intent: { 58 + info: 'text-neutral-foreground-3', 59 + success: 'text-status-success-foreground-1', 60 + warning: 'text-status-warning-foreground-3', 61 + error: 'text-status-danger-foreground-1', 62 + }, 63 + }, 64 + }); 65 + 66 + export interface MessageBarProps { 67 + /** 68 + * intent of the message bar 69 + * @default 'info' 70 + */ 71 + intent?: MessageBarIntent; 72 + /** layout of the message bar */ 73 + layout: MessageBarLayout; 74 + /** optional icon to display */ 75 + icon?: JSXNode; 76 + class?: string; 77 + children?: JSXNode; 78 + } 79 + 80 + /** 81 + * message bar component for displaying inline messages 82 + * @param props.intent the intent/severity of the message 83 + * @param props.layout the layout of the message bar 84 + * @param props.icon optional icon to display 85 + * @param props.class additional CSS classes 86 + */ 87 + const MessageBar = (props: MessageBarProps) => { 88 + const { intent = 'info', layout, icon, class: className, children } = props; 89 + 90 + const contextValue: MessageBarContextValue = { intent, layout }; 91 + const renderedIcon = icon ?? getIntentIcon(intent); 92 + 93 + return ( 94 + <MessageBarContext.Provider value={contextValue}> 95 + <div role="group" aria-live="polite" class={root({ intent, layout, className })}> 96 + <span class={iconStyle({ intent })}>{renderedIcon}</span> 97 + 98 + {children} 99 + </div> 100 + </MessageBarContext.Provider> 101 + ); 102 + }; 103 + 104 + export default MessageBar;
packages/danaus/src/web/primitives/utils/dialog-context.tsx packages/danaus/src/web/primitives/dialog/utils/context.tsx
packages/danaus/src/web/primitives/utils/menu-context.tsx packages/danaus/src/web/primitives/menu/utils/context.tsx
packages/danaus/src/web/primitives/utils/message-bar-context.tsx packages/danaus/src/web/primitives/message-bar/utils/context.tsx
+1 -4
packages/danaus/tsconfig.json
··· 1 1 { 2 2 "files": [], 3 - "references": [ 4 - { "path": "./tsconfig.server.json" }, 5 - { "path": "./tsconfig.client.json" } 6 - ] 3 + "references": [{ "path": "./tsconfig.server.json" }, { "path": "./tsconfig.client.json" }] 7 4 }