gubes mirror. how does this work
1
fork

Configure Feed

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

add some debugging tools

leah a21c815a e7dab98c

+318 -261
+96
neo/src/bits/menu.tsx
··· 1 + import { effect, Signal, signal } from "@preact/signals"; 2 + import { createRef, FunctionalComponent, render } from "preact"; 3 + import { useId } from "preact/hooks"; 4 + import "@css/menu.css"; 5 + import { spring, timeline } from "motion"; 6 + 7 + const animate_in = (elem: Element) => timeline([ 8 + [elem.children[0], { scale: [0, 1] }, { at: 0 }], 9 + [elem, { opacity: [0, 1] }, { at: 0 }], 10 + ], { defaultOptions: { easing: coolspring() } }); 11 + 12 + const coolspring = () => spring({ stiffness: 750, damping: 40 }); 13 + 14 + const animate_out = (elem: Element) => timeline([ 15 + [elem.children[0], { scale: [1, 0] }, { at: 0 }], 16 + [elem, { opacity: [1, 0] }, { at: 0 }], 17 + ], { defaultOptions: { easing: coolspring() } }); 18 + 19 + export interface Menu { 20 + open: (ev?: MouseEvent) => void; 21 + is_open: Signal<boolean>; 22 + } 23 + 24 + export function create_menu(Innards: any, opt?: { 25 + scrim?: boolean, 26 + anchor?: "left" | "right" 27 + }): Menu { 28 + const is_open = signal(false); 29 + const x = signal(0); 30 + const y = signal(0); 31 + 32 + const open = (ev?: MouseEvent) => { 33 + ev?.stopPropagation(); 34 + x.value = ev?.clientX ?? 0; 35 + y.value = ev?.clientY ?? 0; 36 + is_open.value = true; 37 + }; 38 + 39 + const dialog_ref = createRef<HTMLDialogElement>(); 40 + const id = useId(); 41 + 42 + const on_outside_click = async () => { 43 + await animate_out(dialog_ref.current!).finished; 44 + dialog_ref.current?.close(); 45 + }; 46 + 47 + const Component = () => <dialog 48 + class={`popover-menu ${opt?.anchor ? `anchor-${opt.anchor}` : ''}`} 49 + style={`--x: ${x.value}px; --y: ${y.value}px`} 50 + ref={dialog_ref} 51 + > 52 + <div class="innards" onClick={e => e.stopPropagation()}> 53 + <Innards/> 54 + </div> 55 + {opt?.scrim && <div className="scrim" aria-hidden/>} 56 + </dialog>; 57 + 58 + effect(() => { 59 + if (is_open.value) { 60 + const elem = document.createElement("div"); 61 + elem.id = id; 62 + render(<Component/>, document.body.appendChild(elem)); 63 + 64 + dialog_ref.current?.showModal(); 65 + dialog_ref.current?.addEventListener("close", () => is_open.value = false); 66 + dialog_ref.current?.addEventListener("click", on_outside_click); 67 + 68 + animate_in(dialog_ref.current!); 69 + } else { 70 + const elem = document.getElementById(id); 71 + elem?.removeEventListener("click", on_outside_click); 72 + elem?.remove(); 73 + } 74 + }); 75 + 76 + return { 77 + open, is_open, 78 + }; 79 + } 80 + 81 + export const MenuItem 82 + : FunctionalComponent<{ 83 + flavour_text?: string, 84 + destructive?: boolean, 85 + icon?: (props: any) => any, 86 + onClick?: (ev: MouseEvent) => any, 87 + }> 88 + = ({ icon: Icon, children, destructive, flavour_text, onClick }) => <li 89 + class="menu-item" 90 + > 91 + <button onClick={onClick} class={`${destructive ? 'destructive' : ''}`}> 92 + {Icon && <Icon/>} 93 + <span class="label">{children}</span> 94 + {flavour_text && <p>{flavour_text}</p>} 95 + </button> 96 + </li>;
-21
neo/src/bits/menu/list-menu.tsx
··· 1 - import { FunctionalComponent } from "preact"; 2 - 3 - const ListMenu: FunctionalComponent = ({ children }) => { 4 - return <ul class="list-menu"> 5 - {children} 6 - </ul> 7 - } 8 - 9 - export const ListMenuItem: FunctionalComponent<{ 10 - icon?: any, 11 - destructive?: boolean, 12 - onClick?: (e: MouseEvent & { currentTarget: HTMLButtonElement }) => void, 13 - }> = ({ children, icon: Icon, destructive, onClick }) => { 14 - return <li> 15 - <button class={`${destructive ? "danger" : ""}`} onClick={onClick}> 16 - {Icon && <Icon aria-hidden />} {children} 17 - </button> 18 - </li>; 19 - }; 20 - 21 - export default ListMenu;
-3
neo/src/bits/menu/menu-item.tsx
··· 1 - import { ListMenuItem } from "./list-menu"; 2 - 3 - export default ListMenuItem;
-75
neo/src/bits/popover.tsx
··· 1 - import "@css/popover.css"; 2 - 3 - import { Signal, useSignal } from "@preact/signals"; 4 - import { animate, spring } from "motion"; 5 - import { FunctionalComponent } from "preact"; 6 - import { MutableRef, useId, useLayoutEffect, useRef } from "preact/hooks"; 7 - 8 - export const create_popover = (content: any) => { 9 - const opened = useSignal(false); 10 - const ref = useRef<HTMLDialogElement>(); 11 - const rect = useSignal<DOMRect | null>(null) 12 - 13 - useLayoutEffect(() => { 14 - if (opened.value) { 15 - ref.current?.showModal(); 16 - 17 - ref.current?.addEventListener("close", () => { 18 - opened.value = false; 19 - }) 20 - } else { 21 - ref.current?.close(); 22 - } 23 - }, [opened.value]) 24 - 25 - let popover = () => opened.value ? <Popover trigger_rect={rect} dialog={ref}> 26 - {content} 27 - </Popover> : <></> 28 - 29 - return { 30 - open: (e?: MouseEvent & { currentTarget: HTMLButtonElement }) => { 31 - if (e) { 32 - rect.value = e.currentTarget.getBoundingClientRect(); 33 - } 34 - opened.value = true 35 - ref.current?.showModal(); 36 - }, 37 - close: () => opened.value = false, 38 - Popover: popover, 39 - } 40 - } 41 - 42 - export const Popover 43 - : FunctionalComponent<{ 44 - dialog: MutableRef<HTMLDialogElement | undefined>, 45 - trigger_rect: Signal<DOMRect | null>, 46 - }> 47 - = ({ children, dialog, trigger_rect }) => { 48 - const id = useId(); 49 - 50 - const r = trigger_rect.value; 51 - const x = r?.right; 52 - const y = r?.top; 53 - 54 - useLayoutEffect(() => { 55 - animate("#" + id, { 56 - scale: [0, 1], 57 - }, { easing: spring({ stiffness: 200, damping: 11, mass: 0.25 }) }); 58 - }); 59 - 60 - return <dialog 61 - class="popover" 62 - id={id} 63 - style={`--x: ${x}px; --y: ${y}px;`} 64 - //@ts-ignore 65 - ref={dialog} 66 - > 67 - {children} 68 - </dialog>; 69 - } 70 - 71 - export const PopoverInnards: FunctionalComponent = ({ children }) => { 72 - return <div class="popover-innards"> 73 - {children} 74 - </div> 75 - }
+21 -25
neo/src/bits/sidebar/network-section.tsx
··· 10 10 import { IconButton, TextButton } from "../buttons"; 11 11 import ConnectionInfo, { ConnInfoPage } from "../conn-info"; 12 12 import { create_dialog } from "../dialog"; 13 - import ListMenu, { ListMenuItem } from "../menu/list-menu"; 14 - import { create_popover } from "../popover"; 13 + import { create_menu, MenuItem } from "../menu.tsx"; 15 14 import { SidebarLink } from "./sidebar-item"; 16 15 import SidebarSection from "./sidebar-section"; 17 16 import { IrcChannel } from "tubes_core/channel.ts"; ··· 84 83 ? resolve_adapter_icon(adapters.value.find(x => x.id == conn.adapter_id)!) 85 84 : undefined; 86 85 87 - const menu = create_popover(<ListMenu> 88 - <ListMenuItem icon={DMIcon}> 86 + const menu = create_menu(() => <ul> 87 + <MenuItem icon={DMIcon}> 89 88 New Direct Message 90 - </ListMenuItem> 89 + </MenuItem> 91 90 92 91 {conn.$state.value == ConnectionState.Disconnected 93 - ? <ListMenuItem 92 + ? <MenuItem 94 93 onClick={() => conn.connect()} 95 94 icon={ConnectIcon} 96 95 > 97 96 Connect 98 - </ListMenuItem> 97 + </MenuItem> 99 98 100 - : <ListMenuItem 99 + : <MenuItem 101 100 onClick={() => conn.disconnect()} 102 101 icon={DisconnectIcon} 103 102 > 104 103 Disconnect 105 - </ListMenuItem> 104 + </MenuItem> 106 105 } 107 106 108 107 {/* debug menu */} 109 108 {process.env.NODE_ENV == "development" && 110 - <ListMenuItem 109 + <MenuItem 111 110 onClick={() => set_location(`${connection_base(conn)}/debug`)} 112 111 icon={DebugIcon} 113 112 > 114 113 Debug 115 - </ListMenuItem>} 114 + </MenuItem>} 116 115 117 116 118 - {conn.supports.sasl() && <ListMenuItem 117 + {conn.supports.sasl() && <MenuItem 119 118 onClick={() => { 120 119 }} 121 120 icon={LoginIcon} 122 121 > 123 122 Log In 124 - </ListMenuItem>} 123 + </MenuItem>} 125 124 126 - {conn.supports.registration() && <ListMenuItem 125 + {conn.supports.registration() && <MenuItem 127 126 onClick={() => { 128 127 }} 129 128 icon={RegisterIcon} 130 129 > 131 130 Register 132 - </ListMenuItem>} 131 + </MenuItem>} 133 132 134 - <ListMenuItem 133 + <MenuItem 135 134 onClick={() => { 136 135 details_diag.open() 137 136 }} 138 137 icon={InfoIcon} 139 138 > 140 139 Details 141 - </ListMenuItem> 140 + </MenuItem> 142 141 143 - <ListMenuItem 142 + <MenuItem 144 143 onClick={() => { 145 144 }} 146 145 icon={ConfIcon} 147 146 > 148 147 Configure 149 - </ListMenuItem> 148 + </MenuItem> 150 149 151 - <ListMenuItem destructive icon={ArchiveIcon}>Archive</ListMenuItem> 152 - </ListMenu>); 150 + <MenuItem destructive icon={ArchiveIcon}>Archive</MenuItem> 151 + </ul>); 153 152 154 153 const channels = useComputed(() => 155 154 conn.$buffers.value.filter(x => x instanceof IrcChannel) ··· 201 200 <ConfIcon aria-hidden/> 202 201 </IconButton> 203 202 <IconButton title="More Stuff" onClick={(e) => menu.open(e)}><EtcIcon aria-hidden/></IconButton> 204 - 205 - <menu.Popover/> 206 203 </header> 207 204 208 205 {recovering && conn.$state.value != ConnectionState.Connected ··· 261 258 const NickTaken: FunctionalComponent<{ conn: Connection }> = ({ conn }) => { 262 259 const reason = conn.$error.value?.[1]; 263 260 const reason_text = reason?.params?.at(-1); 264 - const update = create_popover(<> 261 + const update = create_menu(() => <> 265 262 <p class="body-small low-emphasis"> 266 263 Someone else on {conn.label} is already using the 267 264 nickname <i>{conn.nickname}</i>. ··· 284 281 <div class="buttons"> 285 282 <TextButton onClick={() => conn.connect()}>Try Again</TextButton> 286 283 <TextButton onClick={update.open}>Update Nickname</TextButton> 287 - <update.Popover/> 288 284 </div> 289 285 </>; 290 286 }
+60 -42
neo/src/buffer/view.tsx
··· 12 12 import Trangle from "~icons/ph/triangle-fill"; 13 13 import MessageInput from "./input"; 14 14 import { squish_messages } from "./squisher"; 15 + import dump from "@src/debug/dump"; 15 16 16 17 import MembersIcon from "~icons/ph/users"; 17 18 import { IconButton } from "@src/bits/buttons"; ··· 33 34 } 34 35 35 36 export const BufferContext = createContext<ChatBuffer | null>(null); 37 + 38 + function dump_view(buffer: ChatBuffer, msgs: IrcMessage[]) { 39 + dump({ 40 + buffer, 41 + msgs 42 + }); 43 + } 36 44 37 45 const BufferView: FunctionalComponent<{ buffer: ChatBuffer }> = ({ buffer }) => { 38 46 const msgs = useSignal<IrcMessage[]>([]); ··· 73 81 if (marker.focused.value) to_bottom(); 74 82 }, [msgs.value]); 75 83 84 + const DebugMenu = create_menu(() => <ul> 85 + <MenuItem onClick={() => dump_view(buffer, msgs.value)}>Dump Current View</MenuItem> 86 + </ul>, { anchor: "right" }); 87 + 76 88 return <BufferContext.Provider value={buffer}> 77 89 <div class={`message-list ${message_style.value}`}> 78 - {buffer instanceof IrcChannel ? <ChannelHeader channel={buffer} /> : <div />} 90 + {buffer instanceof IrcChannel ? <ChannelHeader channel={buffer} debug_menu={DebugMenu}/> : <div/>} 79 91 {finished_init.value 80 92 ? <MessageList 81 93 list_elem={list_elem} ··· 89 101 marker.focused.value = elem.scrollHeight <= elem.scrollTop + elem.clientHeight + padding; 90 102 }} 91 103 /> 92 - : <SkeletonLoader ref2={list_elem} /> 104 + : <SkeletonLoader ref2={list_elem}/> 93 105 } 94 - {marker.unread_count.value != 0 && <UnreadBanner marker={marker} />} 106 + {marker.unread_count.value != 0 && <UnreadBanner marker={marker}/>} 95 107 <MessageInput 96 108 is_scrolled={!marker.focused.value} 97 109 onSubmit={async (text, is_cmd) => { ··· 117 129 118 130 export const MessageList 119 131 : FunctionalComponent<{ 120 - list_elem: RefObject<HTMLUListElement>, 121 - buffer: ChatBuffer, 122 - is_loading: Signal<boolean>, 123 - msgs: Signal<IrcMessage[]>, 124 - onScroll: HTMLProps<HTMLUListElement>['onScroll'], 125 - }> 132 + list_elem: RefObject<HTMLUListElement>, 133 + buffer: ChatBuffer, 134 + is_loading: Signal<boolean>, 135 + msgs: Signal<IrcMessage[]>, 136 + onScroll: HTMLProps<HTMLUListElement>['onScroll'], 137 + }> 126 138 = ({ list_elem, is_loading, onScroll, buffer, msgs }) => { 127 - const squished = squish_messages(buffer, msgs.value); 128 - return <ul class="messages" ref={list_elem} onScroll={onScroll}> 129 - {/* header. todo: should not be in a ul element */} 130 - <StartOfHistory buffer={buffer} /> 139 + const squished = squish_messages(buffer, msgs.value); 140 + return <ul class="messages" ref={list_elem} onScroll={onScroll}> 141 + {/* header. todo: should not be in a ul element */} 142 + <StartOfHistory buffer={buffer}/> 131 143 132 - {/* history getting bits */} 133 - {is_loading.value && "loadin"} 134 - {/* <LoadTrigger onIntersect={async () => {}} parent={list_elem} /> */} 144 + {/* history getting bits */} 145 + {is_loading.value && "loadin"} 146 + {/* <LoadTrigger onIntersect={async () => {}} parent={list_elem} /> */} 135 147 136 - {/* the messages themselves */} 137 - {squished} 138 - </ul>; 139 - } 148 + {/* the messages themselves */} 149 + {squished} 150 + </ul>; 151 + } 140 152 141 153 142 154 const UnreadBanner 143 155 : FunctionalComponent<{ marker: ReadMarker }> 144 156 = ({ marker }) => <div class="unread-banner"> 145 - <span class="number">{marker.unread_count}</span> 146 - unread {marker.unread_count.value == 1 ? "message" : "messages"} 147 - </div> 157 + <span class="number">{marker.unread_count}</span> 158 + unread {marker.unread_count.value == 1 ? "message" : "messages"} 159 + </div> 148 160 149 161 const ChannelHeader 150 - : FunctionalComponent<{ channel: IrcChannel }> 151 - = ({ channel }) => <header class="channel-header"> 152 - <h1> 153 - {channel.name} 154 - </h1> 155 - <p> 156 - {channel.$topic.value} 157 - </p> 158 - <MemberCount count={channel.$members.value.length} /> 159 - </header> 162 + : FunctionalComponent<{ channel: IrcChannel, debug_menu: Menu }> 163 + = ({ channel, debug_menu }) => <header class="channel-header"> 164 + <h1> 165 + {channel.name} 166 + </h1> 167 + <p> 168 + {channel.$topic.value} 169 + </p> 170 + {process.env.NODE_ENV == "development" && 171 + <IconButton onClick={debug_menu.open}><DebugIcon/></IconButton> 172 + } 173 + <MemberCount count={channel.$members.value.length}/> 174 + </header> 160 175 161 176 const MemberCount: FunctionalComponent<{ count: number }> = ({ count }) => { 162 - return <IconButton><MembersIcon />{count}</IconButton> 177 + return <IconButton><MembersIcon/>{count}</IconButton> 163 178 } 164 179 180 + import DebugIcon from "~icons/ph/hammer"; 181 + import { create_menu, Menu, MenuItem } from "@src/bits/menu.tsx"; 182 + 165 183 const StartOfHistory: FunctionalComponent<{ buffer: ChatBuffer }> = ({ buffer }) => { 166 184 const chathistory = buffer.conn.capabilities.has("draft/chathistory"); 167 185 return <div className="message-list-start"> ··· 169 187 <p class="subtitle">this is the start of the channel's logs.</p> 170 188 {!chathistory && 171 189 <p class="chathistory-warning"> 172 - <Trangle aria-hidden /> Messages sent while Tubes was closed might not be shown. 190 + <Trangle aria-hidden/> Messages sent while Tubes was closed might not be shown. 173 191 <a href="/">Learn More</a> 174 192 </p>} 175 193 ··· 179 197 const SkeletonLoader 180 198 : FunctionalComponent<{ ref2: RefObject<HTMLUListElement> }> 181 199 = ({ ref2 }) => <ul ref={ref2} class="messages skeleton"> 182 - {Array.from({ length: 50 }, () => { 183 - return <li aria-hidden> 184 - <span class="name">{"h".repeat(Math.floor(Math.random() * 10))}</span> 185 - <p>{"h ".repeat(Math.floor(Math.random() * 75))}</p> 186 - </li> 187 - })} 188 - </ul> 200 + {Array.from({ length: 50 }, () => { 201 + return <li aria-hidden> 202 + <span class="name">{"h".repeat(Math.floor(Math.random() * 10))}</span> 203 + <p>{"h ".repeat(Math.floor(Math.random() * 75))}</p> 204 + </li> 205 + })} 206 + </ul> 189 207 190 208 export default BufferView;
+8 -1
neo/src/chat/storage.ts
··· 63 63 } 64 64 65 65 async before(id: Date | string, target: string, conn: Connection, limit: number = 50) { 66 - const trans = this.db.transaction("messages", "readwrite"); 66 + const trans = this.db.transaction("messages", "readonly"); 67 67 const store = trans.objectStore("messages"); 68 68 69 69 if (id instanceof Date) { ··· 96 96 } else { 97 97 return []; 98 98 } 99 + } 100 + 101 + async __debug_get_everything() { 102 + const trans = this.db.transaction("messages", "readonly"); 103 + const store = trans.objectStore("messages"); 104 + 105 + return store.getAll(); 99 106 } 100 107 } 101 108
-1
neo/src/css/debug.css
··· 1 1 article.debug { 2 2 padding-bottom: 2rem; 3 3 4 - 5 4 h2 { 6 5 font-size: 6vw; 7 6 font-weight: 200;
+106
neo/src/css/menu.css
··· 1 + .popover-menu { 2 + --backdrop: 0; 3 + 4 + position: fixed; 5 + top: 0; 6 + left: 0; 7 + 8 + transform: translateX(var(--x)) translateY(var(--y)); 9 + 10 + &.anchor-right { 11 + transform: translateX(calc(var(--x) - 100%)) translateY(var(--y)); 12 + } 13 + 14 + margin: 0; 15 + padding: 0; 16 + border: 0; 17 + background-color: transparent; 18 + overflow: visible; 19 + perspective: 75rem; 20 + 21 + > div.innards { 22 + position: relative; 23 + z-index: 1; 24 + 25 + background-color: white; 26 + border: 1px solid var(--colour-grey-200); 27 + border-radius: 6px; 28 + box-shadow: 0px 2px 8px var(--colour-grey-200); 29 + transform-origin: top left; 30 + 31 + .anchor-right & { 32 + transform-origin: top right; 33 + } 34 + } 35 + 36 + > div.innards ul { 37 + list-style: none; 38 + margin: 0; 39 + width: max-content; 40 + padding: .25rem; 41 + 42 + display: grid; 43 + /* flex-direction: column; */ 44 + } 45 + 46 + &::backdrop { 47 + opacity: 0; 48 + } 49 + 50 + div.scrim { 51 + position: fixed; 52 + top: calc(var(--y) * -1); 53 + left: calc(var(--x) * -1); 54 + 55 + z-index: 0; 56 + 57 + background-color: rgba(0 0 0 / 0.25); 58 + 59 + width: 100vw; 60 + height: 100vh; 61 + } 62 + } 63 + 64 + .menu-item > button { 65 + padding: .375rem; 66 + border-radius: 4px; 67 + padding-right: 1rem; 68 + width: 100%; 69 + 70 + background-color: transparent; 71 + border: none; 72 + 73 + display: flex; 74 + align-items: center; 75 + gap: .375rem; 76 + 77 + font: inherit; 78 + font-size: .85rem; 79 + font-variation-settings: 'GRAD' 100; 80 + font-weight: 400; 81 + color: var(--colour-grey-800); 82 + 83 + cursor: pointer; 84 + 85 + svg { 86 + width: 16px; 87 + height: 16px; 88 + } 89 + 90 + &.destructive { 91 + color: var(--colour-red-700); 92 + 93 + &:hover { 94 + color: var(--colour-red-800); 95 + } 96 + } 97 + 98 + &:hover { 99 + background-color: var(--colour-grey-50); 100 + color: var(--colour-grey-950); 101 + } 102 + 103 + &:active { 104 + background-color: var(--colour-grey-100); 105 + } 106 + }
+4 -1
neo/src/css/messages.css
··· 407 407 408 408 .icon-button { 409 409 align-self: center; 410 - margin-left: auto; 411 410 412 411 svg { 413 412 width: 16px; 414 413 height: 16px; 414 + } 415 + 416 + &:first-of-type { 417 + margin-left: auto; 415 418 } 416 419 } 417 420 }
-92
neo/src/css/popover.css
··· 1 - .popover { 2 - margin: 0; 3 - padding: 0; 4 - border: 0; 5 - overflow: visible; 6 - background-color: transparent; 7 - 8 - position: absolute; 9 - left: var(--x); 10 - top: var(--y); 11 - 12 - transform-origin: top left; 13 - 14 - &::backdrop { 15 - background-color: rgba(0, 0, 0, 0.1); 16 - } 17 - 18 - &:has(.list-menu)::backdrop { 19 - background-color: transparent; 20 - } 21 - } 22 - 23 - .popover-innards { 24 - padding: .75rem; 25 - min-width: 18rem; 26 - width: 28rem; 27 - 28 - background-color: white; 29 - border: 1px solid var(--colour-grey-200); 30 - border-radius: 6px; 31 - box-shadow: 0px 2px 8px var(--colour-grey-200); 32 - } 33 - 34 - .list-menu { 35 - padding: .25rem; 36 - list-style: none; 37 - 38 - background-color: white; 39 - border: 1px solid var(--colour-grey-200); 40 - border-radius: 8px; 41 - box-shadow: 0px 2px 8px var(--colour-grey-200); 42 - 43 - display: flex; 44 - flex-direction: column; 45 - gap: .1rem; 46 - 47 - min-width: 12rem; 48 - 49 - > li > button { 50 - padding: .375rem; 51 - border-radius: 4px; 52 - padding-right: 1rem; 53 - width: 100%; 54 - 55 - background-color: transparent; 56 - border: none; 57 - 58 - display: flex; 59 - align-items: center; 60 - gap: .375rem; 61 - 62 - font: inherit; 63 - font-size: .85rem; 64 - font-variation-settings: 'GRAD' 100; 65 - font-weight: 400; 66 - color: var(--colour-grey-800); 67 - 68 - cursor: pointer; 69 - 70 - svg { 71 - width: 16px; 72 - height: 16px; 73 - } 74 - 75 - &.danger { 76 - color: var(--colour-red-700); 77 - 78 - &:hover { 79 - color: var(--colour-red-800); 80 - } 81 - } 82 - 83 - &:hover { 84 - background-color: var(--colour-grey-50); 85 - color: var(--colour-grey-950); 86 - } 87 - 88 - &:active { 89 - background-color: var(--colour-grey-100); 90 - } 91 - } 92 - }
+16
neo/src/debug/dump.ts
··· 1 + export default function dump(data: unknown) { 2 + const seen: any[] = []; 3 + const json = JSON.stringify(data, (_, val) => { 4 + if (val != null && typeof val == "object") { 5 + if (seen.indexOf(val) >= 0) { 6 + return; 7 + } 8 + seen.push(val); 9 + } 10 + return val; 11 + }); 12 + 13 + const blob = new Blob([json], { type: "application/json" }); 14 + 15 + window.open(URL.createObjectURL(blob)); 16 + }
+7
neo/src/debug/index.tsx
··· 2 2 import { FunctionalComponent } from "preact"; 3 3 import { Connection } from "tubes_core"; 4 4 import { PrimaryButton } from "@src/bits/buttons.tsx"; 5 + import storage from "@src/chat/storage.ts"; 6 + import dump from "@src/debug/dump.ts"; 5 7 6 8 const DebugView: FunctionalComponent<{ conn: Connection }> = ({ conn }) => <article class="settings"> 7 9 <hgroup> ··· 14 16 <div class="panel"> 15 17 <PrimaryButton onClick={() => conn.__debug_explode?.()}> 16 18 Simulate Disconnect 19 + </PrimaryButton> 20 + <PrimaryButton 21 + onClick={() => storage.__debug_get_everything().then(x => dump(x)) } 22 + > 23 + Dump IndexedDB 17 24 </PrimaryButton> 18 25 </div> 19 26 </section>