gubes mirror. how does this work
1
fork

Configure Feed

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

add say gex

leah be69ac89 7abec6e7

+410 -50
+1
core/capabilities.ts
··· 9 9 "sasl", 10 10 "draft/chathistory", 11 11 "draft/event-playback", 12 + "draft/account-registration", 12 13 "soju.im/bouncer-networks", 13 14 ] as const; 14 15
+11 -2
core/connection.ts
··· 1 - import { Signal, signal } from "@preact/signals-core"; 1 + import { computed, Signal, signal } from "@preact/signals-core"; 2 2 import { nanoid } from "nanoid"; 3 3 import { IrcChannel } from "./channel"; 4 4 import { MessageHandler, default_handler } from "./handler"; ··· 6 6 import { ISupport } from "./isupport"; 7 7 import { IrcMessage } from "./parser"; 8 8 import { Matcher, TaskQueue } from "./queue"; 9 - import { SaslConfig } from "./sasl"; 9 + import { Sasl, SaslConfig } from "./sasl"; 10 10 import { IrcChannelState, Numeric, to_casemap_lowercase } from "./support"; 11 11 import list_channels from "./list"; 12 12 import { ChatBuffer } from "./buffer"; ··· 150 150 available_capabilities: Map<string, string | null> = new Map(); 151 151 152 152 $buffers: Signal<ChatBuffer[]> = signal([]); 153 + 153 154 $state = signal<ConnectionState>(ConnectionState.Disconnected); 155 + $connecting = computed(() => 156 + this.$state.value == ConnectionState.Connecting || 157 + this.$state.value == ConnectionState.Registering 158 + ); 159 + $connected = computed(() => this.$state.value == ConnectionState.Connected); 160 + 154 161 $error: Signal<[code: ConnectionErrorCode, reason?: IrcMessage] | null> = signal(null); 162 + 163 + sasl?: Sasl; 155 164 156 165 abstract connect(): Promise<void>; 157 166
+7 -5
core/sasl.ts
··· 38 38 constructor(public conn: Connection, public stuff: SaslConfig) { 39 39 } 40 40 41 + authed = false; 42 + 41 43 async authenticate() { 42 44 this.check_security(); 43 45 const mechs = this.get_mechs() 44 46 45 - let authed = false; 47 + this.authed = false; 46 48 47 49 if (this.stuff.password) { 48 50 // password-based auth 49 51 // if (!authed && mechs.includes("SCRAM-SHA-256")) throw "todo" 50 - if (!authed && mechs.includes("PLAIN")) { 51 - authed = await this.plain(this.stuff.username, this.stuff.password) 52 + if (!this.authed && mechs.includes("PLAIN")) { 53 + this.authed = await this.plain(this.stuff.username, this.stuff.password) 52 54 } 53 55 } else if (this.stuff.cert) { 54 56 // password-cringe auth 55 57 // if (!authed && mechs.includes("EXTERNAL")) throw "todo" 56 58 } 57 59 58 - if (!authed) { 60 + if (!this.authed) { 59 61 // abort auth 60 62 this.conn.send("AUTHENTICATE *"); 61 63 } 62 64 63 - return authed; 65 + return this.authed; 64 66 } 65 67 66 68 private check_security() {
+2 -1
core/ws/connection.ts
··· 66 66 67 67 if (this.config.sasl?.username) { 68 68 try { 69 - await new Sasl(this, this.config.sasl).authenticate(); 69 + this.sasl = new Sasl(this, this.config.sasl); 70 + await this.sasl?.authenticate(); 70 71 } catch (e) { 71 72 if (!(e instanceof IrcMessage)) throw e; 72 73
+51
neo/src/bits/actions.tsx
··· 1 + import { styled } from "goober" 2 + import { FunctionComponent } from "preact" 3 + import { HTMLProps } from "preact/compat" 4 + 5 + export const ActionGroup = styled("ul")` 6 + overflow: hidden; 7 + border-radius: 6px; 8 + list-style: none; 9 + display: flex; 10 + flex-direction: column; 11 + padding: 0; 12 + margin: 0 -0.25rem; 13 + ` 14 + 15 + export const ActionItem 16 + : FunctionComponent<HTMLProps<HTMLButtonElement> & { disabled?: boolean }> 17 + = (props) => <li> 18 + {styled("button")` 19 + all: unset; 20 + cursor: pointer; 21 + user-select: none; 22 + -webkit-user-select: none; 23 + 24 + width: 100%; 25 + padding: .5rem .75rem; 26 + font-size: .9rem; 27 + 28 + display: flex; 29 + gap: .5rem; 30 + background-color: var(--colour-accent-100); 31 + 32 + &:hover { 33 + background-color: var(--colour-accent-200); 34 + } 35 + 36 + &:disabled { 37 + background-color: var(--colour-grey-100); 38 + color: var(--colour-grey-500); 39 + cursor: not-allowed; 40 + 41 + &:hover { 42 + background-color: var(--colour-grey-100); 43 + } 44 + } 45 + 46 + svg { 47 + width: 16px; 48 + height: 16px; 49 + } 50 + `(props)} 51 + </li>
+8 -2
neo/src/bits/buttons.tsx
··· 5 5 type Button = FunctionalComponent<HTMLProps<HTMLButtonElement> 6 6 // webstorm doesn't like it if you don't make this explicit. 7 7 // don't ask me why 8 - & { type?: "submit" | "reset" | "button" | undefined }>; 8 + & { 9 + type?: "submit" | "reset" | "button" | undefined; 10 + disabled?: boolean; 11 + }>; 9 12 10 13 export const IconButton: Button = ({ children, ...rest }) => 11 14 <button {...rest} class={`icon-button ${rest["class"] ?? ""}`}> ··· 18 21 </button> 19 22 20 23 export const PrimaryButton: Button = ({ children, ...rest }) => 21 - <button {...rest} class={`primary-button ${rest["class"] ?? ""}`}> 24 + <button 25 + {...rest} 26 + class={`primary-button ${rest["class"] ?? ""}`} 27 + > 22 28 {children} 23 29 </button> 24 30
+1 -1
neo/src/bits/form/form-field.tsx
··· 2 2 import { FunctionalComponent } from "preact"; 3 3 4 4 const FormField 5 - : FunctionalComponent<{ label: string, flavour_text?: string }> 5 + : FunctionalComponent<{ label: string, flavour_text?: any }> 6 6 = ({ label, flavour_text, children }) => { 7 7 return <label class="form-field"> 8 8 <span>{label}</span>
+2 -3
neo/src/bits/sidebar/network-section.tsx
··· 134 134 </MenuItem> 135 135 136 136 <MenuItem 137 - onClick={() => { 138 - }} 137 + onClick={() => set_location(`${connection_base(conn)}/configure`)} 139 138 icon={ConfIcon} 140 139 > 141 140 Configure ··· 176 175 </IconButton> 177 176 } 178 177 <IconButton 179 - onClick={() => set_location(`${connection_base(conn)}/config`)} 178 + onClick={() => set_location(`${connection_base(conn)}/configure`)} 180 179 title="Configure" 181 180 > 182 181 <ConfIcon aria-hidden />
+10
neo/src/css/buttons.css
··· 80 80 background-color: var(--colour-grey-200); 81 81 } 82 82 } 83 + 84 + &:disabled { 85 + background-color: var(--colour-grey-100); 86 + color: var(--colour-grey-500); 87 + cursor: not-allowed; 88 + 89 + &:hover { 90 + background-color: var(--colour-grey-100); 91 + } 92 + } 83 93 } 84 94 85 95 .button-row {
+42 -5
neo/src/css/network-info.css
··· 1 1 .network-info { 2 + --width: 56rem; 2 3 display: grid; 3 4 grid-auto-rows: max-content; 4 - grid-template-columns: 1fr 1fr 1fr; 5 - max-width: 56rem; 5 + grid-template-columns: 2fr 1fr; 6 + width: 100%; 6 7 padding: 0 1rem; 7 - gap: 1rem; 8 + gap: 1.5rem; 8 9 margin: auto; 10 + padding-bottom: 4rem; 11 + padding: 0 max(1rem, calc((100% - var(--width)) / 2)); 12 + 13 + @media screen and (max-width: 56rem) { 14 + grid-template-columns: 1fr; 15 + 16 + * { 17 + grid-column: 1 !important; 18 + } 19 + 20 + .right { 21 + grid-row: 2; 22 + } 23 + 24 + .left { 25 + margin-top: 4rem; 26 + grid-row: 3; 27 + } 28 + } 29 + 30 + .left { 31 + grid-column: 1; 32 + } 33 + 34 + .right { 35 + grid-column: 2; 36 + } 9 37 10 38 hgroup { 11 39 grid-column: 1 / -1; ··· 20 48 21 49 h1 { 22 50 font-weight: 100; 23 - font-size: 3rem; 51 + font-size: clamp(2rem, 6vw, 4rem); 24 52 font-stretch: ultra-expanded; 25 53 font-variation-settings: 'GRAD' 50; 26 54 text-align: center; ··· 34 62 margin: 0; 35 63 font-size: 1rem; 36 64 font-weight: 500; 65 + color: var(--colour-grey-500); 66 + font-size: clamp(.75rem, 1.75vw, 1rem); 37 67 38 68 &.connected { 39 69 color: var(--colour-ectoplasm-700); ··· 43 73 44 74 .motd { 45 75 grid-column: 1 / span 2; 76 + grid-row: 2; 46 77 position: relative; 78 + margin-top: -1.1rem; 47 79 48 80 pre { 49 81 background-color: var(--colour-grey-50); 82 + margin-top: .75rem; 50 83 border-radius: 8px; 51 84 padding: .75rem; 52 85 white-space: pre-line; ··· 56 89 position: absolute; 57 90 left: 0; 58 91 right: 0; 59 - bottom: -2rem; 92 + bottom: -2.5rem; 60 93 61 94 margin: auto; 62 95 width: max-content; ··· 73 106 74 107 mask-image: linear-gradient(to bottom, black, transparent 90%); 75 108 } 109 + } 110 + 111 + &.expanded pre { 112 + margin-bottom: 4rem; 76 113 } 77 114 } 78 115 }
+19 -20
neo/src/main.tsx
··· 10 10 import DebugView from "@src/debug"; 11 11 import SettingsPage from '@src/settings'; 12 12 import { accent, build_accent_css } from '@src/support'; 13 - import { render } from 'preact'; 14 - import { Route, Router, Switch } from 'wouter-preact'; 13 + import { h, render } from 'preact'; 14 + import { Route, Router, Switch, useLocation } from 'wouter-preact'; 15 15 import { useHashLocation } from "wouter-preact/use-hash-location"; 16 16 import JoinChannelPage from './pages/join-channel'; 17 17 import Spinner from "@src/bits/spinner.tsx"; 18 18 import NetworkInfoPage from './pages/network-info'; 19 + import * as goober from 'goober'; 20 + import { Connection } from 'tubes_core'; 21 + import NetworkConfigurationPage from './pages/network-conf'; 19 22 23 + goober.setup(h); 20 24 Connections.initialise(); 21 25 22 26 export function Tubes() { ··· 29 33 <Route path="/"> 30 34 <NoBufferPlaceholder /> 31 35 </Route> 32 - <Route path="/connection/:adapter?/:id/debug"> 33 - {({ adapter, id }) => { 34 - const conn = Connections.fetch(id, adapter); 35 - return conn ? <DebugView conn={conn} /> : "one sec"; 36 - }} 37 - </Route> 38 36 <Route path="/connection/:adapter?/:id/channel/:target"> 39 - {({ adapter, id, target }) => { 37 + {({ target, id, adapter }) => { 40 38 const conn = Connections.fetch(id, adapter); 41 39 const chan = conn?.get_channel(target); 42 40 return chan ··· 44 42 : <Spinner />; 45 43 }} 46 44 </Route> 47 - <Route path="/connection/:adapter?/:id/join"> 45 + <Route path="/connection/:adapter?/:id/*"> 48 46 {({ adapter, id }) => { 49 47 const conn = Connections.fetch(id, adapter); 50 48 51 49 return conn 52 - ? <JoinChannelPage conn={conn} /> 53 - : <Spinner />; 54 - }} 55 - </Route> 56 - <Route path="/connection/:adapter?/:id/info"> 57 - {({ adapter, id }) => { 58 - const conn = Connections.fetch(id, adapter); 59 - 60 - return conn 61 - ? <NetworkInfoPage conn={conn} /> 50 + ? <ConnectionRoutes conn={conn} /> 62 51 : <Spinner />; 63 52 }} 64 53 </Route> ··· 68 57 </Switch> 69 58 </main> 70 59 </Router> 60 + } 61 + 62 + function ConnectionRoutes({ conn }: { conn: Connection }) { 63 + console.log(useLocation()) 64 + return <Switch> 65 + <Route path="/connection/:adapter?/:id/info"><NetworkInfoPage conn={conn} /></Route> 66 + <Route path="/connection/:adapter?/:id/join"><JoinChannelPage conn={conn} /></Route> 67 + <Route path="/connection/:adapter?/:id/debug"><DebugView conn={conn} /></Route> 68 + <Route path="/connection/:adapter?/:id/configure"><NetworkConfigurationPage conn={conn} /></Route> 69 + </Switch> 71 70 } 72 71 73 72 render(<Tubes />, document.getElementById('app')!);
+77
neo/src/pages/network-conf.tsx
··· 1 + import { IconButton } from "@src/bits/buttons"; 2 + import FormField from "@src/bits/form/form-field"; 3 + import { connection_base } from "@src/chat/conns"; 4 + import { css } from "goober"; 5 + import { FunctionalComponent } from "preact"; 6 + import { Connection } from "tubes_core"; 7 + import { useLocation } from "wouter-preact"; 8 + 9 + import BackIcon from "~icons/ph/arrow-left"; 10 + 11 + const NetworkHeader: FunctionalComponent<{ conn: Connection }> = ({ conn, children }) => { 12 + const [, setLocation] = useLocation(); 13 + return <hgroup class={css` 14 + padding: 0 max(1rem, calc((100% - var(--width)) / 2)); 15 + margin-top: 2.5rem; 16 + margin-bottom: 1rem; 17 + 18 + display: grid; 19 + grid-template-columns: max-content max-content; 20 + grid-template-rows: auto auto; 21 + height: max-content; 22 + align-items: center; 23 + gap: .25rem .5rem; 24 + color: var(--colour-grey-700); 25 + 26 + p { 27 + grid-column: 2; 28 + margin: 0; 29 + font-size: 0.85rem; 30 + color: var(--colour-grey-600); 31 + font-variation-settings: 'GRAD' 150; 32 + } 33 + h1 { 34 + grid-column: 2; 35 + 36 + font-size: 1.25rem; 37 + font-weight: 450; 38 + margin: 0; 39 + } 40 + 41 + button { 42 + grid-column: 1; 43 + grid-row: 2; 44 + 45 + svg { 46 + width: 18px; 47 + height: 18px; 48 + } 49 + } 50 + `}> 51 + <IconButton 52 + title="Back" 53 + onClick={() => setLocation(connection_base(conn) + "/info")} 54 + > 55 + <BackIcon /> 56 + </IconButton> 57 + {children} 58 + </hgroup> 59 + } 60 + 61 + export default function NetworkConfigurationPage({ conn }: { conn: Connection }) { 62 + return <main style="--width: 56rem;"> 63 + <NetworkHeader conn={conn}> 64 + <p>{conn.label}</p> 65 + <h1>Configuration</h1> 66 + </NetworkHeader> 67 + <form class={css` 68 + display: grid; 69 + grid-template-columns: 1fr 1fr; 70 + padding: 0 max(1rem, calc((100% - var(--width)) / 2)); 71 + `}> 72 + <FormField label="URL" flavour_text={<></>}> 73 + <input type="url" value={String(conn.url)} /> 74 + </FormField> 75 + </form> 76 + </main> 77 + }
+161 -11
neo/src/pages/network-info.tsx
··· 1 1 import "@css/network-info.css"; 2 2 import { useComputed, useSignal } from "@preact/signals"; 3 + import { ActionGroup, ActionItem } from "@src/bits/actions"; 3 4 import { PrimaryButton } from "@src/bits/buttons"; 5 + import { css } from "goober"; 4 6 import { Connection } from "tubes_core"; 5 7 import { ConnectionState } from "tubes_core/connection"; 8 + import { FunctionalComponent } from "preact"; 9 + import { useLocation } from "wouter-preact"; 10 + import { connection_base } from "@src/chat/conns"; 6 11 7 12 export default function NetworkInfoPage({ conn }: { conn: Connection }) { 8 13 const status = useComputed(() => { 9 14 switch (conn.$state.value) { 10 - case ConnectionState.Disconnected: return <>"disconnected"</>; 11 - case ConnectionState.Disconnecting: return "disconnected"; 12 - case ConnectionState.Connecting: return "connecting"; 13 - case ConnectionState.Registering: return "connecting"; 15 + case ConnectionState.Disconnected: return <>disconnected</>; 16 + case ConnectionState.Disconnecting: return <>disconnecting</>; 17 + case ConnectionState.Connecting: return <>connecting…</>; 18 + case ConnectionState.Registering: return <>connecting…</>; 14 19 case ConnectionState.Connected: return <>connected</>; 15 - case ConnectionState.Failed: return "uh oh!"; 20 + case ConnectionState.Failed: return <>oh dear</>; 16 21 } 17 22 }); 18 - const connected = useComputed(() => conn.$state.value == ConnectionState.Connected) 19 23 20 24 return <article class="network-info"> 21 25 <hgroup> 22 26 <h1>{conn.label}</h1> 23 - <p class={`status ${connected.value ? "connected" : "disconnected"}`}>{status}</p> 27 + <p class={`status ${conn.$connected.value ? "connected" : "disconnected"}`}>{status}</p> 24 28 </hgroup> 25 - 26 - {conn.motd && <Motd motd={conn.motd} />} 27 - 29 + <div class="left"> 30 + {conn.$connected.value && conn.motd && <Motd motd={conn.motd} />} 31 + {conn.$connected.value && <ServerMessages conn={conn} />} 32 + {!conn.$connected.value && <Placeholder> 33 + connect to {conn.label} to see some beautiful information here 34 + <PrimaryButton onClick={() => conn.connect()} disabled={conn.$connecting.value}> 35 + Connect 36 + </PrimaryButton> 37 + </Placeholder>} 38 + </div> 39 + <div class="right"> 40 + <NetworkActions conn={conn} /> 41 + <AccountSection conn={conn} /> 42 + </div> 28 43 29 44 </article> 30 45 } ··· 32 47 const Motd = ({ motd }: { motd: string }) => { 33 48 const expanded = useSignal(false); 34 49 return <section class={`motd ${expanded.value ? "expanded" : ""}`}> 35 - <h2 class="heading">Message of the Day</h2> 50 + <h2 class="heading" style="margin-top: 0;">Message of the Day</h2> 36 51 <pre> 37 52 {motd} 38 53 </pre> ··· 40 55 {expanded.value ? "Collapse" : "Expand"} 41 56 </PrimaryButton> 42 57 </section> 58 + } 59 + 60 + const ServerMessages: FunctionalComponent<{ conn: Connection }> = ({ conn }) => <> 61 + <h2 class="heading">Server Messages</h2> 62 + <Placeholder> 63 + {conn.label} has nothing to say right now. 64 + </Placeholder> 65 + </> 66 + 67 + const Placeholder: FunctionalComponent = ({ children }) => 68 + <div class={css` 69 + border: 1px dashed var(--colour-grey-200); 70 + border-radius: 8px; 71 + height: 8rem; 72 + margin-top: .75rem; 73 + padding: 1rem; 74 + 75 + display: flex; 76 + align-items: center; 77 + justify-content: center; 78 + flex-direction: column; 79 + gap: 0.5rem; 80 + 81 + color: var(--colour-grey-500); 82 + font-style: italic; 83 + font-size: .85rem; 84 + font-variation-settings: 'GRAD' 150; 85 + text-align: center; 86 + text-wrap: balance; 87 + `}> 88 + {children} 89 + </div> 90 + 91 + 92 + import DisconnectIcon from "~icons/ph/plug"; 93 + import ConnectIcon from "~icons/ph/plugs-connected"; 94 + import ConfigureIcon from "~icons/ph/wrench"; 95 + // import DmIcon from "~icons/ph/paper-plane-right"; 96 + // import JoinChannelIcon from "~icons/ph/plus"; 97 + 98 + const NetworkActions: FunctionalComponent<{ conn: Connection }> = ({ conn }) => { 99 + const [, setLocation] = useLocation(); 100 + return <section class={css` 101 + margin-top: .75rem; 102 + `}> 103 + <ActionGroup> 104 + <ActionItem 105 + onClick={() => conn.$connected.value 106 + ? conn.disconnect() 107 + : conn.connect()} 108 + disabled={conn.$connecting.value} 109 + > 110 + {conn.$connected.value 111 + ? <><DisconnectIcon /> Disconnect</> 112 + : <><ConnectIcon /> Connect</>} 113 + </ActionItem> 114 + <ActionItem onClick={() => setLocation(`${connection_base(conn)}/configure`)}><ConfigureIcon /> Configure</ActionItem> 115 + </ActionGroup> 116 + </section> 117 + } 118 + 119 + const account_section = css` 120 + margin-top: 1.5rem; 121 + 122 + .login-info { 123 + font-size: .8rem; 124 + color: var(--colour-grey-600); 125 + line-height: 1.5; 126 + margin-top: 1.5rem; 127 + margin-bottom: .5rem; 128 + 129 + i, b { 130 + color: var(--colour-grey-800); 131 + } 132 + 133 + b { 134 + font-weight: 500; 135 + } 136 + } 137 + `; 138 + 139 + import EditProfileIcon from "~icons/ph/pen"; 140 + import LoginIcon from "~icons/ph/sign-in"; 141 + import RegisterIcon from "~icons/ph/user-plus"; 142 + import { pick_colour } from "@src/chat/colours"; 143 + 144 + const AccountSection: FunctionalComponent<{ conn: Connection }> = ({ conn }) => { 145 + if (conn.supports.sasl() && conn.sasl?.authed) { 146 + return <section class={account_section}> 147 + <h2 class="heading">About You</h2> 148 + </section> 149 + } 150 + if (conn.supports.sasl()) { 151 + return <section class={account_section}> 152 + <h2 class="heading">About You</h2> 153 + <ProfileDetails conn={conn} /> 154 + <p class="login-info"> 155 + You are not logged into an account on <i>{conn.label}</i>. 156 + </p> 157 + <ActionGroup> 158 + <ActionItem><LoginIcon />Log In</ActionItem> 159 + {conn.supports.registration() && 160 + <ActionItem><RegisterIcon /> Register</ActionItem>} 161 + </ActionGroup> 162 + </section> 163 + } 164 + } 165 + 166 + const ProfileDetails: FunctionalComponent<{ conn: Connection }> = ({ conn }) => { 167 + const nick_colour = pick_colour(conn.nickname); 168 + 169 + return <div> 170 + <p class={css` 171 + color: var(--colour-${nick_colour}-600); 172 + font-variation-settings: 'GRAD' 150; 173 + font-size: .95rem; 174 + font-weight: 500; 175 + margin-top: 1rem; 176 + margin-bottom: .25rem; 177 + `}> 178 + {conn.nickname} 179 + </p> 180 + <p class={css` 181 + color: var(--colour-grey-600); 182 + font-variation-settings: 'GRAD' 150; 183 + font-size: .8rem; 184 + margin: .25rem 0; 185 + margin-bottom: .5rem; 186 + `}> 187 + {conn.realname} 188 + </p> 189 + <ActionGroup> 190 + <ActionItem><EditProfileIcon />Edit Profile</ActionItem> 191 + </ActionGroup> 192 + </div> 43 193 }
+17
package-lock.json
··· 13 13 ], 14 14 "dependencies": { 15 15 "@preact/signals": "^1.3.0", 16 + "goober": "^2.1.16", 16 17 "idb": "^8.0.0" 17 18 } 18 19 }, ··· 1979 1980 "url": "https://github.com/sponsors/fb55" 1980 1981 } 1981 1982 }, 1983 + "node_modules/csstype": { 1984 + "version": "3.1.3", 1985 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 1986 + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 1987 + "license": "MIT", 1988 + "peer": true 1989 + }, 1982 1990 "node_modules/dayjs": { 1983 1991 "version": "1.11.13", 1984 1992 "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", ··· 2226 2234 "license": "MIT", 2227 2235 "engines": { 2228 2236 "node": ">=4" 2237 + } 2238 + }, 2239 + "node_modules/goober": { 2240 + "version": "2.1.16", 2241 + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", 2242 + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", 2243 + "license": "MIT", 2244 + "peerDependencies": { 2245 + "csstype": "^3.0.10" 2229 2246 } 2230 2247 }, 2231 2248 "node_modules/happy-dom": {
+1
package.json
··· 9 9 ], 10 10 "dependencies": { 11 11 "@preact/signals": "^1.3.0", 12 + "goober": "^2.1.16", 12 13 "idb": "^8.0.0" 13 14 }, 14 15 "scripts": {