Add DM UI shell components

This commit is contained in:
thePR0M3TH3AN committed 2026-01-18 23:27:29 -05:00
1 parent 99808e2407
commit 11853037ec
12 files changed
+1032 -2

No files matched your search

+2 -2
View File
@@ -1,7 +1,7 @@
:root{--surface-base:var(--color-surface);--surface-alt:var(--color-surface-alt);--surface-raised:var(--color-card);--surface-raised-private:var(--color-card-private);--surface-raised-critical:var(--color-card-critical);--surface-panel:var(--color-panel);--surface-panel-hover:var(--color-panel-hover);--surface-sunken:var(--color-surface-sunken);--surface-muted:var(--color-surface-muted);--surface-veil:var(--color-overlay-veil);--surface-inverse:var(--color-page);--surface-overlay:var(--color-overlay);--surface-overlay-muted:var(--color-overlay-muted);--surface-overlay-strong:var(--color-overlay-strong);--surface-overlay-darker:var(--color-overlay-darker);--surface-overlay-black:var(--color-overlay-black);--surface-overlay-banner-base:var(--color-page);--surface-overlay-banner:var(--surface-overlay-banner-base);--surface-overlay-banner-opaque:var(--surface-overlay-banner-base);--surface-overlay-banner-strong:color-mix(in srgb,var(--surface-overlay-banner-base) 82%,transparent);--surface-overlay-banner-bottom:color-mix(in srgb,var(--surface-overlay-banner-base) 50%,transparent);--surface-overlay-banner-channel-mid:color-mix(in srgb,var(--surface-overlay-banner-base) 24%,transparent);--surface-overlay-banner-channel-faint:color-mix(in srgb,var(--surface-overlay-banner-base) 10%,transparent);--surface-overlay-banner-soft:color-mix(in srgb,var(--surface-overlay-banner-base) 56%,transparent);--surface-overlay-banner-faint:color-mix(in srgb,var(--surface-overlay-banner-base) 18%,transparent);--surface-overlay-banner-transparent:transparent;--surface-overlay-banner-gradient-header:linear-gradient(to bottom,var(--surface-overlay-banner-opaque) 0%,var(--surface-overlay-banner-strong) 38%,var(--surface-overlay-banner-soft) 70%,var(--surface-overlay-banner-faint) 88%,var(--surface-overlay-banner-transparent) 100%);--surface-overlay-banner-gradient-channel:linear-gradient(to top,var(--surface-overlay-banner-bottom) 0%,var(--surface-overlay-banner-channel-mid) 28%,var(--surface-overlay-banner-channel-faint) 42%,var(--surface-overlay-banner-transparent) 50%);--surface-overlay-banner-gradient:var(
--surface-overlay-banner-gradient-header
);--surface-overlay-panel:var(--color-overlay-panel);--surface-overlay-panel-soft:var(--color-overlay-panel-soft);--surface-overlay-panel-tint:var(--color-overlay-panel-tint);--surface-overlay-panel-glass:var(--color-overlay-panel-glass);--surface-overlay-panel-glass-soft:var(--color-overlay-panel-glass-soft);--surface-modal-backdrop:var(--color-modal-backdrop);--surface-modal:var(--color-modal-surface);--blur-modal-backdrop:18px;--blur-modal-alert:14px;--blur-thumbnail-blur:18px;--modal-nav-backdrop-blur:6px;--app-header-centerline-offset:calc(var(--space-sm) + var(--space-2xl-plus)/2);--border-default:var(--color-border);--border-subtle:var(--color-border-subtle);--border-strong:var(--color-border-translucent-stronger);--border-overlay:var(--color-border-translucent);--border-overlay-strong:var(--color-border-translucent-strongest);--border-overlay-glow:var(--color-border-translucent-glow);--border-neutral-soft:var(--color-border-neutral-soft);--border-info-soft:var(--color-border-info-soft);--border-sidebar-strong:var(--color-border-sidebar-strong);--border-width-hairline:1px;--border-width-fine:0.5px;--text-primary:var(--color-text);--text-inverse:var(--color-text-inverse);--text-muted:var(--color-text-muted);--text-muted-strong:var(--color-muted-strong);--text-subtle:var(--color-subtle-text);--text-frosted-primary:var(--color-text-frosted);--text-frosted-strong:var(--color-text-frosted-strong);--text-frosted-soft:var(--color-text-frosted-soft);--status-info:var(--color-info);--status-info-strong:var(--color-info-strong);--status-info-surface:var(--color-info-glass);--status-info-border:var(--color-info-border);--status-info-on:var(--color-info-on);--status-success:var(--color-success);--status-success-strong:var(--color-success-strong);--status-success-surface:var(--color-success-surface);--status-success-border:var(--color-success-border);--status-success-on:var(--color-success-on);--status-warning:var(--color-warning);--status-warning-strong:var(--color-warning-strong);--status-warning-surface:var(--color-warning-surface);--status-warning-border:var(--color-warning-border);--status-warning-on:var(--color-warning-on);--status-danger:var(--color-critical);--status-danger-strong:var(--color-critical-strong);--status-danger-surface:var(--color-danger-surface);--status-danger-border:var(--color-danger-border);--status-danger-on:var(--color-danger-on);--status-private:var(--color-private);--status-private-strong:var(--color-private-strong);--status-private-surface:var(--color-private-surface);--status-private-border:var(--color-private-border);--status-private-on:var(--color-private-on);--status-neutral:var(--color-muted);--status-neutral-strong:var(--color-muted-strong);--status-neutral-surface:var(--color-neutral-surface);--status-neutral-border:var(--color-neutral-border);--status-neutral-on:var(--color-neutral-on);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-app-header:40;--z-overlay-nav:60;--z-overlay-floating:70;--z-overlay-popover:140;--z-overlay-toast:180;--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--z-modal-top-root:140;--z-modal-top-overlay:150;--z-modal-top-content:160;--z-modal-peak-root:165;--z-modal-peak-overlay:175;--z-modal-peak-content:185;--overlay-backdrop-color:var(--color-overlay-backdrop);--overlay-backdrop-blur:12px;--sidebar-width-expanded-base:16rem;--sidebar-dropup-content-width:0px;--sidebar-width-expanded:max(var(--sidebar-width-expanded-base),var(--sidebar-dropup-content-width));--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-sm);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-popover-padding-inline:0.65rem;--space-sidebar-popover-gap:0.45rem;--space-sidebar-dropup-scroll-reserve:calc(var(--scrollbar-thin-width) + var(--space-sm) + var(--space-2xs));--sidebar-dropup-max-height:18rem;--space-sidebar-nav-padding-block:0.7rem;--space-sidebar-nav-padding-inline-start:var(--space-xs);--space-sidebar-nav-padding-inline-end:calc(var(--space-lg) + var(--space-2xs));--space-sidebar-subnav-padding-block:0.55rem;--space-sidebar-nav-translate:0.35rem;--grid-card-min-width:20rem;--breakpoint-lg:64rem;--popover-backdrop-blur:12px;--overlay-panel-radius:var(--radius-lg);--overlay-panel-shadow:var(--shadow-lg);--overlay-panel-padding-inline:var(--popup-padding-inline);--overlay-panel-padding-blocLine truncated
/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:80%}@media (min-width:768px){html{font-size:100%}}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{--app-header-centerline:calc(env(safe-area-inset-top, 0) + var(--app-header-centerline-offset));font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}:root[data-theme-accent=override]{--color-accent:var(--color-accent-override);--color-accent-strong:var(--color-accent-strong-override);--color-accent-pressed:var(--color-accent-pressed-override)}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar{pointer-events:none;opacity:0;visibility:hidden;z-index:var(--z-sidebar-overlay)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:block}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.app-shell__header{position:sticky;top:env(safe-area-inset-top,0);z-index:var(--z-app-header);padding-top:calc(var(--space-sm) + env(safe-area-inset-top, 0));padding-bottom:vaLine truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:80%}@media (min-width:768px){html{font-size:100%}}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{--app-header-centerline:calc(env(safe-area-inset-top, 0) + var(--app-header-centerline-offset));font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}:root[data-theme-accent=override]{--color-accent:var(--color-accent-override);--color-accent-strong:var(--color-accent-strong-override);--color-accent-pressed:var(--color-accent-pressed-override)}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar{pointer-events:none;opacity:0;visibility:hidden;z-index:var(--z-sidebar-overlay)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:block}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.app-shell__header{position:sticky;top:env(safe-area-inset-top,0);z-index:var(--z-app-header);padding-top:calc(var(--space-sm) + env(safe-area-inset-top, 0));padding-bottom:vaLine truncated
--motion-video-card-backdrop-translate
);--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:var(--blur-video-card-backdrop-strong)}.\!card:focus-within,.\!card:hover{--video-card-lift-translate-y:var(--motion-video-card-hover-translate)!important;--video-card-backdrop-opacity:0.425!important;--video-card-backdrop-translate-y:var(
--motion-video-card-backdrop-translate
@@ -11,4 +11,4 @@
--player-modal-primary-inline-padding-lg,clamp(var(--space-sm),2.75vw,var(--space-lg))
)}}.revert-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.revert-modal__layout{grid-template-columns:minmax(var(--revert-modal-sidebar-min),var(--revert-modal-sidebar-width)) minmax(0,1fr)}}#playerModal.video-modal__primary{display:block}.video-modal__primary{display:flex;flex-direction:column;gap:1.5rem;min-width:0;min-height:0}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem;min-width:0;min-height:0;width:100%;align-self:stretch}.video-modal__similar{display:none}@media (min-width:1024px){#playerModal.video-modal__similar{display:block}.video-modal__similar{display:flex;flex-direction:column;gap:1.5rem;align-self:stretch;flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module{height:100%;display:flex;flex:1 1 auto;min-height:0;flex-direction:column}#playerModal .video-modal__similar .player-modal__module-list{flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module-scroll{height:100%;max-height:none;min-height:0}}#playerModal .player-modal__module-item{width:100%}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max))!important}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media,#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))!important}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-2xs),.85vw,var(--space-sm))}.player-modal__similar-card{outline:2px solid transparent;outline-offset:2px}.player-modal__similar-card:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.player-modal__similar-card{display:block}.player-modal__similar-card{position:relative;displayLine truncated
--motion-video-card-backdrop-translate
);--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:var(--blur-video-card-backdrop-strong);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.watch-history-card[data-state=private]{background-color:var(--surface-raised-private);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg)}.watch-history-card[data-state=disabled],.watch-history-card[data-state=private]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=disabled]{opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.watch-history-card[data-alert=nsfw-owner]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--color-black)}.watch-history-card[data-alert=nsfw-owner]:hover{--tw-border-opacity:0.6;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card img[data-thumbnail-state=blurred],.watch-history-card img[data-visual-state=blurred]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}.watch-history-card[data-owner-is-viewer=true][data-url-health-state=offline][data-stream-health-state=unhealthy]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max));flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}.watch-history-card[data-motion=enter]{opacity:0;animation:card-motion-enter .22s ease-out forwards}#playerModal.watch-history-card{display:block}.watch-history-card{display:flex;flex-wrap:wrap;align-items:stretch;gap:1.25rem;padding:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}@media (prefers-reduced-motion:reduce){.watch-history-card,.watch-history-card[data-motion=enter]{transition-property:none}.watch-history-card[data-motion=enter]{opacity:1;animation:none}}.watch-history-card:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.watch-history-card{gap:1.5rem;padding:1.25rem}}.watch-history-card--private{border-color:var(--color-info);--tw-border-opacity:0.6}#playerModal.watch-histoLine truncated
);--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:var(--blur-video-card-backdrop-strong);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.watch-history-card[data-state=private]{background-color:var(--surface-raised-private);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg)}.watch-history-card[data-state=disabled],.watch-history-card[data-state=private]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=disabled]{opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.watch-history-card[data-alert=nsfw-owner]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--color-black)}.watch-history-card[data-alert=nsfw-owner]:hover{--tw-border-opacity:0.6;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card img[data-thumbnail-state=blurred],.watch-history-card img[data-visual-state=blurred]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}.watch-history-card[data-owner-is-viewer=true][data-url-health-state=offline][data-stream-health-state=unhealthy]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max));flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}.watch-history-card[data-motion=enter]{opacity:0;animation:card-motion-enter .22s ease-out forwards}#playerModal.watch-history-card{display:block}.watch-history-card{display:flex;flex-wrap:wrap;align-items:stretch;gap:1.25rem;padding:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}@media (prefers-reduced-motion:reduce){.watch-history-card,.watch-history-card[data-motion=enter]{transition-property:none}.watch-history-card[data-motion=enter]{opacity:1;animation:none}}.watch-history-card:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.watch-history-card{gap:1.5rem;padding:1.25rem}}.watch-history-card--private{border-color:var(--color-info);--tw-border-opacity:0.6}#playerModal.watch-histoLine truncated
+397
View File
@@ -4976,3 +4976,400 @@
}
}
}
@layer components {
.dm-app-shell {
display: flex;
flex-direction: column;
gap: var(--space-lg);
padding: var(--space-lg);
background: var(--surface-base);
color: var(--text-primary);
min-height: 100vh;
}
.dm-app-shell__sidebar,
.dm-app-shell__main {
border-radius: var(--radius-lg);
border: var(--border-width-hairline) solid var(--border-default);
background: var(--surface-panel);
box-shadow: var(--shadow-soft);
}
.dm-app-shell__sidebar {
display: flex;
flex-direction: column;
gap: var(--space-md);
padding: var(--space-md);
}
.dm-app-shell__title {
font-size: var(--font-size-lg);
margin: 0;
}
.dm-app-shell__main {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-md);
padding: var(--space-md);
background: var(--surface-raised);
}
.dm-conversation-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.dm-conversation-list__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.dm-conversation-list__title {
font-size: var(--font-size-sm);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin: 0;
}
.dm-conversation-list__items {
display: flex;
flex-direction: column;
gap: var(--space-xs);
overflow-y: auto;
max-height: 60vh;
padding-right: var(--space-xs);
}
.dm-conversation-list__state {
font-size: var(--font-size-sm);
color: var(--text-muted);
padding: var(--space-md);
text-align: center;
background: var(--surface-sunken);
border-radius: var(--radius-md);
}
.dm-contact-row {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-md);
border: var(--border-width-hairline) solid var(--border-subtle);
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}
.dm-contact-row:hover,
.dm-contact-row:focus-visible {
background: var(--surface-muted);
border-color: var(--border-strong);
outline: none;
}
.dm-contact-row--active {
background: var(--surface-sunken);
border-color: var(--border-strong);
}
.dm-contact-row__content {
display: flex;
flex-direction: column;
gap: var(--space-2xs);
flex: 1;
}
.dm-contact-row__header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--space-xs);
}
.dm-contact-row__name {
font-weight: 600;
}
.dm-contact-row__time {
font-size: var(--font-size-2xs);
color: var(--text-muted);
}
.dm-contact-row__preview {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.dm-contact-row__badge {
align-self: flex-start;
padding: var(--space-3xs) var(--space-xs);
border-radius: var(--radius-full);
background: var(--surface-overlay);
color: var(--text-inverse);
font-size: var(--font-size-3xs);
}
.dm-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-full);
background: var(--surface-sunken);
color: var(--text-muted);
font-weight: 600;
overflow: hidden;
border: var(--border-width-hairline) solid var(--border-subtle);
}
.dm-avatar--md {
width: 2.5rem;
height: 2.5rem;
}
.dm-avatar--sm {
width: 2rem;
height: 2rem;
}
.dm-avatar__image {
width: 100%;
height: 100%;
object-fit: cover;
}
.dm-message-thread {
display: flex;
flex-direction: column;
gap: var(--space-sm);
flex: 1;
min-height: 0;
}
.dm-message-thread__header {
display: flex;
align-items: baseline;
gap: var(--space-sm);
}
.dm-message-thread__title {
font-size: var(--font-size-base);
margin: 0;
}
.dm-message-thread__status {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.dm-message-thread__timeline {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-sm);
border-radius: var(--radius-md);
background: var(--surface-panel);
border: var(--border-width-hairline) solid var(--border-subtle);
overflow-y: auto;
min-height: 16rem;
}
.dm-message-thread__state {
padding: var(--space-md);
text-align: center;
color: var(--text-muted);
background: var(--surface-sunken);
border-radius: var(--radius-md);
}
.dm-message-bubble {
max-width: 85%;
display: flex;
flex-direction: column;
gap: var(--space-2xs);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-lg);
border: var(--border-width-hairline) solid var(--border-subtle);
background: var(--surface-sunken);
}
.dm-message-bubble--incoming {
align-self: flex-start;
}
.dm-message-bubble--outgoing {
align-self: flex-end;
background: var(--surface-overlay-panel);
}
.dm-message-bubble__content {
font-size: var(--font-size-sm);
}
.dm-message-bubble__meta {
display: flex;
gap: var(--space-xs);
font-size: var(--font-size-2xs);
color: var(--text-muted);
}
.dm-message-bubble__status {
color: var(--status-danger);
}
.dm-day-divider {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: var(--space-xs);
color: var(--text-muted);
font-size: var(--font-size-2xs);
}
.dm-day-divider__line {
height: 1px;
background: var(--border-subtle);
}
.dm-notification-center {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.dm-notification-center__empty {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.dm-notification-center__item {
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-md);
border: var(--border-width-hairline) solid var(--border-subtle);
background: var(--surface-sunken);
font-size: var(--font-size-xs);
}
.dm-notification-center__item--info {
background: var(--status-info-surface);
border-color: var(--status-info-border);
color: var(--status-info-on);
}
.dm-notification-center__item--success {
background: var(--status-success-surface);
border-color: var(--status-success-border);
color: var(--status-success-on);
}
.dm-notification-center__item--warning {
background: var(--status-warning-surface);
border-color: var(--status-warning-border);
color: var(--status-warning-on);
}
.dm-notification-center__item--error {
background: var(--status-danger-surface);
border-color: var(--status-danger-border);
color: var(--status-danger-on);
}
.dm-composer {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.dm-composer__label {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.dm-composer__input {
resize: vertical;
border-radius: var(--radius-md);
border: var(--border-width-hairline) solid var(--border-default);
background: var(--surface-base);
color: var(--text-primary);
padding: var(--space-sm);
font-family: inherit;
}
.dm-composer__input:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.dm-composer__actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-sm);
}
.dm-composer__hint {
font-size: var(--font-size-2xs);
color: var(--text-muted);
}
.dm-composer__send {
border-radius: var(--radius-md);
border: var(--border-width-hairline) solid var(--border-strong);
background: var(--surface-overlay-panel);
color: var(--text-primary);
padding: var(--space-xs) var(--space-md);
font-weight: 600;
cursor: pointer;
}
.dm-composer__send:hover,
.dm-composer__send:focus-visible {
background: var(--surface-overlay-panel-soft);
outline: none;
}
.dm-composer__status {
font-size: var(--font-size-2xs);
color: var(--status-danger);
min-height: 1rem;
}
@media (min-width: theme("screens.md")) {
.dm-app-shell {
flex-direction: row;
align-items: stretch;
}
.dm-app-shell__sidebar {
flex: 0 0 18rem;
max-width: 20rem;
height: calc(100vh - var(--space-lg) * 2);
}
.dm-app-shell__main {
height: calc(100vh - var(--space-lg) * 2);
}
.dm-conversation-list__items {
max-height: none;
flex: 1;
}
}
@media (min-width: theme("screens.lg")) {
.dm-app-shell__sidebar {
flex-basis: 20rem;
}
.dm-message-thread__timeline {
padding: var(--space-md);
}
}
}
+125
View File
@@ -0,0 +1,125 @@
import { Composer } from "./Composer.js";
import { ConversationList } from "./ConversationList.js";
import { MessageThread } from "./MessageThread.js";
import { NotificationCenter } from "./NotificationCenter.js";
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
const DEFAULT_CONVERSATIONS = [
{
id: "conv-1",
name: "Nia Nova",
preview: "The relay looks stable now.",
timestamp: "2m",
unreadCount: 2,
status: "Online",
avatarSrc: "",
},
{
id: "conv-2",
name: "Orbit Ops",
preview: "Can you share the stream link?",
timestamp: "1h",
unreadCount: 0,
status: "Away",
avatarSrc: "",
},
];
const DEFAULT_MESSAGES = [
{ type: "day", label: "Today" },
{
id: "msg-1",
direction: "incoming",
body: "Hey! The new DM view looks promising.",
timestamp: "09:12",
},
{
id: "msg-2",
direction: "outgoing",
body: "Thanks! I'm wiring up the layout and keyboard flow now.",
timestamp: "09:14",
status: "sending",
},
];
export class AppShell {
constructor({
document: doc,
conversations = DEFAULT_CONVERSATIONS,
activeConversationId = DEFAULT_CONVERSATIONS[0]?.id || "",
conversationState = "idle",
messages = DEFAULT_MESSAGES,
threadState = "idle",
threadErrorType = "",
composerState = "idle",
notifications = [],
onSelectConversation,
onSendMessage,
} = {}) {
if (!doc) {
throw new Error("AppShell requires a document reference.");
}
this.document = doc;
this.root = createElement(doc, "div", "dm-app-shell");
const sidebar = createElement(doc, "aside", "dm-app-shell__sidebar");
const sidebarTitle = createElement(
doc,
"h1",
"dm-app-shell__title",
"Direct Messages",
);
sidebar.appendChild(sidebarTitle);
sidebar.appendChild(
ConversationList({
document: doc,
conversations,
activeId: activeConversationId,
state: conversationState,
onSelect: onSelectConversation,
}),
);
const main = createElement(doc, "main", "dm-app-shell__main");
main.appendChild(
NotificationCenter({
document: doc,
notices: notifications,
}),
);
main.appendChild(
MessageThread({
document: doc,
contact: conversations.find((conversation) => conversation.id === activeConversationId) || {},
messages,
state: threadState,
errorType: threadErrorType,
}),
);
main.appendChild(
Composer({
document: doc,
state: composerState,
onSend: onSendMessage,
}),
);
this.root.appendChild(sidebar);
this.root.appendChild(main);
}
getRoot() {
return this.root;
}
}
+51
View File
@@ -0,0 +1,51 @@
const DEFAULT_AVATAR_ALT = "Avatar";
function getInitials(label = "") {
if (typeof label !== "string") {
return "";
}
const words = label.trim().split(/\s+/).filter(Boolean);
if (!words.length) {
return "";
}
const initials = words.slice(0, 2).map((word) => word[0].toUpperCase());
return initials.join("");
}
export function Avatar({
document: doc,
src = "",
alt = DEFAULT_AVATAR_ALT,
size = "md",
initials = "",
status = "",
} = {}) {
if (!doc) {
throw new Error("Avatar requires a document reference.");
}
const avatar = doc.createElement("div");
avatar.className = `dm-avatar dm-avatar--${size}`;
if (status) {
avatar.dataset.status = status;
}
const resolvedInitials = initials || getInitials(alt);
if (src) {
const img = doc.createElement("img");
img.className = "dm-avatar__image";
img.alt = alt || DEFAULT_AVATAR_ALT;
img.loading = "lazy";
img.decoding = "async";
img.src = src;
avatar.appendChild(img);
} else {
const fallback = doc.createElement("span");
fallback.className = "dm-avatar__fallback";
fallback.textContent = resolvedInitials || "?";
avatar.appendChild(fallback);
}
return avatar;
}
+72
View File
@@ -0,0 +1,72 @@
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
export function Composer({
document: doc,
placeholder = "Write a message…",
state = "idle",
onSend,
} = {}) {
if (!doc) {
throw new Error("Composer requires a document reference.");
}
const form = createElement(doc, "form", "dm-composer");
form.noValidate = true;
const label = createElement(doc, "label", "dm-composer__label", "Message");
label.setAttribute("for", "dm-composer-input");
const textarea = createElement(doc, "textarea", "dm-composer__input");
textarea.id = "dm-composer-input";
textarea.placeholder = placeholder;
textarea.rows = 2;
const actions = createElement(doc, "div", "dm-composer__actions");
const hint = createElement(
doc,
"span",
"dm-composer__hint",
"Press Ctrl/⌘ + Enter to send",
);
const button = createElement(doc, "button", "dm-composer__send", "Send");
button.type = "submit";
actions.appendChild(hint);
actions.appendChild(button);
const status = createElement(doc, "div", "dm-composer__status");
if (state === "error") {
status.textContent = "Send failed. Try again.";
} else if (state === "sending") {
status.textContent = "Sending…";
}
form.appendChild(label);
form.appendChild(textarea);
form.appendChild(actions);
form.appendChild(status);
form.addEventListener("submit", (event) => {
event.preventDefault();
if (typeof onSend === "function") {
onSend(textarea.value);
}
});
textarea.addEventListener("keydown", (event) => {
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
form.requestSubmit();
}
});
return form;
}
+85
View File
@@ -0,0 +1,85 @@
import { Avatar } from "./Avatar.js";
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
export function ContactRow({
document: doc,
contact = {},
isActive = false,
tabIndex = -1,
onSelect,
} = {}) {
if (!doc) {
throw new Error("ContactRow requires a document reference.");
}
const {
id = "",
name = "Unknown",
preview = "",
timestamp = "",
unreadCount = 0,
avatarSrc = "",
status = "",
} = contact;
const button = createElement(doc, "button", "dm-contact-row");
button.type = "button";
button.dataset.conversationId = id;
button.setAttribute("role", "option");
button.setAttribute("aria-selected", String(Boolean(isActive)));
button.tabIndex = tabIndex;
if (isActive) {
button.classList.add("dm-contact-row--active");
}
const avatar = Avatar({
document: doc,
src: avatarSrc,
alt: name,
initials: name,
status,
});
button.appendChild(avatar);
const content = createElement(doc, "div", "dm-contact-row__content");
const header = createElement(doc, "div", "dm-contact-row__header");
const nameEl = createElement(doc, "span", "dm-contact-row__name", name);
header.appendChild(nameEl);
const timeEl = createElement(doc, "span", "dm-contact-row__time", timestamp);
header.appendChild(timeEl);
content.appendChild(header);
const previewEl = createElement(
doc,
"div",
"dm-contact-row__preview",
preview,
);
content.appendChild(previewEl);
if (unreadCount > 0) {
const badge = createElement(doc, "span", "dm-contact-row__badge");
badge.textContent = `${unreadCount}`;
badge.setAttribute("aria-label", `${unreadCount} unread messages`);
content.appendChild(badge);
}
button.appendChild(content);
if (typeof onSelect === "function") {
button.addEventListener("click", () => onSelect(contact));
}
return button;
}
+96
View File
@@ -0,0 +1,96 @@
import { ContactRow } from "./ContactRow.js";
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
function getFocusableItems(listEl) {
return [...listEl.querySelectorAll("[data-conversation-id]")];
}
export function ConversationList({
document: doc,
conversations = [],
activeId = "",
state = "idle",
onSelect,
} = {}) {
if (!doc) {
throw new Error("ConversationList requires a document reference.");
}
const root = createElement(doc, "section", "dm-conversation-list");
const header = createElement(doc, "div", "dm-conversation-list__header");
header.appendChild(createElement(doc, "h2", "dm-conversation-list__title", "Messages"));
root.appendChild(header);
const list = createElement(doc, "div", "dm-conversation-list__items");
list.setAttribute("role", "listbox");
list.setAttribute("aria-label", "Conversation list");
const renderState = createElement(doc, "div", "dm-conversation-list__state");
if (state === "loading") {
renderState.textContent = "Loading conversations…";
list.appendChild(renderState);
} else if (state === "empty") {
renderState.textContent = "No conversations yet.";
list.appendChild(renderState);
} else if (state === "error") {
renderState.textContent = "We couldn't load conversations.";
list.appendChild(renderState);
} else {
const resolvedActiveId = activeId || (conversations[0] ? conversations[0].id : "");
conversations.forEach((conversation, index) => {
const isActive = conversation.id === resolvedActiveId;
const row = ContactRow({
document: doc,
contact: conversation,
isActive,
tabIndex: isActive || (!resolvedActiveId && index === 0) ? 0 : -1,
onSelect,
});
list.appendChild(row);
});
}
list.addEventListener("keydown", (event) => {
const items = getFocusableItems(list);
if (!items.length) {
return;
}
const currentIndex = items.indexOf(doc.activeElement);
let nextIndex = currentIndex;
switch (event.key) {
case "ArrowDown":
nextIndex = Math.min(items.length - 1, currentIndex + 1);
break;
case "ArrowUp":
nextIndex = Math.max(0, currentIndex - 1);
break;
case "Home":
nextIndex = 0;
break;
case "End":
nextIndex = items.length - 1;
break;
default:
return;
}
event.preventDefault();
items[nextIndex].focus();
});
root.appendChild(list);
return root;
}
+24
View File
@@ -0,0 +1,24 @@
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
export function DayDivider({ document: doc, label = "" } = {}) {
if (!doc) {
throw new Error("DayDivider requires a document reference.");
}
const divider = createElement(doc, "div", "dm-day-divider");
const line = createElement(doc, "span", "dm-day-divider__line");
const text = createElement(doc, "span", "dm-day-divider__label", label);
divider.appendChild(line);
divider.appendChild(text);
divider.appendChild(line.cloneNode());
return divider;
}
+51
View File
@@ -0,0 +1,51 @@
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
export function MessageBubble({
document: doc,
message = {},
variant = "incoming",
} = {}) {
if (!doc) {
throw new Error("MessageBubble requires a document reference.");
}
const {
body = "",
timestamp = "",
status = "sent",
} = message;
const wrapper = createElement(doc, "div", "dm-message-bubble");
wrapper.classList.add(`dm-message-bubble--${variant}`);
wrapper.dataset.status = status;
const content = createElement(doc, "div", "dm-message-bubble__content", body);
wrapper.appendChild(content);
const meta = createElement(doc, "div", "dm-message-bubble__meta");
if (timestamp) {
meta.appendChild(createElement(doc, "span", "dm-message-bubble__time", timestamp));
}
if (status === "failed") {
meta.appendChild(
createElement(doc, "span", "dm-message-bubble__status", "Send failed"),
);
} else if (status === "sending") {
meta.appendChild(
createElement(doc, "span", "dm-message-bubble__status", "Sending…"),
);
}
wrapper.appendChild(meta);
return wrapper;
}
+80
View File
@@ -0,0 +1,80 @@
import { DayDivider } from "./DayDivider.js";
import { MessageBubble } from "./MessageBubble.js";
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
export function MessageThread({
document: doc,
contact = {},
messages = [],
state = "idle",
errorType = "",
} = {}) {
if (!doc) {
throw new Error("MessageThread requires a document reference.");
}
const root = createElement(doc, "section", "dm-message-thread");
const header = createElement(doc, "header", "dm-message-thread__header");
header.appendChild(
createElement(
doc,
"h3",
"dm-message-thread__title",
contact.name || "Direct Messages",
),
);
if (contact.status) {
header.appendChild(
createElement(doc, "span", "dm-message-thread__status", contact.status),
);
}
root.appendChild(header);
const timeline = createElement(doc, "div", "dm-message-thread__timeline");
timeline.setAttribute("role", "log");
timeline.setAttribute("aria-live", "polite");
const stateEl = createElement(doc, "div", "dm-message-thread__state");
if (state === "loading") {
stateEl.textContent = "Decrypting messages…";
timeline.appendChild(stateEl);
} else if (state === "empty") {
stateEl.textContent = "Say hello to start the conversation.";
timeline.appendChild(stateEl);
} else if (state === "error") {
const errorMessage =
errorType === "decrypt"
? "We couldn't decrypt some messages."
: "Something went wrong loading this thread.";
stateEl.textContent = errorMessage;
timeline.appendChild(stateEl);
} else {
messages.forEach((message) => {
if (message.type === "day") {
timeline.appendChild(DayDivider({ document: doc, label: message.label }));
return;
}
const bubble = MessageBubble({
document: doc,
message,
variant: message.direction === "outgoing" ? "outgoing" : "incoming",
});
timeline.appendChild(bubble);
});
}
root.appendChild(timeline);
return root;
}
+40
View File
@@ -0,0 +1,40 @@
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
export function NotificationCenter({ document: doc, notices = [] } = {}) {
if (!doc) {
throw new Error("NotificationCenter requires a document reference.");
}
const root = createElement(doc, "section", "dm-notification-center");
if (!notices.length) {
const placeholder = createElement(
doc,
"div",
"dm-notification-center__empty",
"You're all caught up.",
);
root.appendChild(placeholder);
return root;
}
notices.forEach((notice) => {
const item = createElement(doc, "div", "dm-notification-center__item");
if (notice.variant) {
item.classList.add(`dm-notification-center__item--${notice.variant}`);
}
item.textContent = notice.message || "";
root.appendChild(item);
});
return root;
}
+9
View File
@@ -0,0 +1,9 @@
export { AppShell } from "./AppShell.js";
export { Avatar } from "./Avatar.js";
export { ContactRow } from "./ContactRow.js";
export { ConversationList } from "./ConversationList.js";
export { MessageThread } from "./MessageThread.js";
export { MessageBubble } from "./MessageBubble.js";
export { Composer } from "./Composer.js";
export { NotificationCenter } from "./NotificationCenter.js";
export { DayDivider } from "./DayDivider.js";