Merge pull request #5175 from urbit/hm/grid-system-preferences

grid: implementing preferences menu ui
This commit is contained in:
Hunter Miller 2021-08-30 18:42:28 -05:00 committed by GitHub
commit 86875ad483
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 550 additions and 32 deletions

11
.vercel/README.txt Normal file
View File

@ -0,0 +1,11 @@
> Why do I have a folder named ".vercel" in my project?
The ".vercel" folder is created when you link a directory to a Vercel project.
> What does the "project.json" file contain?
The "project.json" file contains:
- The ID of the Vercel project that you linked ("projectId")
- The ID of the user or team your Vercel project is owned by ("orgId")
> Should I commit the ".vercel" folder?
No, you should not share the ".vercel" folder with anyone.
Upon creation, it will be automatically added to your ".gitignore" file.

1
.vercel/project.json Normal file
View File

@ -0,0 +1 @@
{"orgId":"EDiU8DZExvM9N4unZGYQbG3d","projectId":"prj_fbAU5smemBgtr5t8lsk5ZoT9zNtI"}

View File

@ -51,7 +51,23 @@ module.exports = {
'react/jsx-props-no-spreading': 'off',
'react/require-default-props': 'off',
'import/no-extraneous-dependencies': ['error'],
'tailwind/class-order': 'off'
'tailwind/class-order': 'off',
'jsx-a11y/label-has-associated-control': [
'error',
{
required: {
some: ['nesting', 'id']
}
}
],
'jsx-a11y/label-has-for': [
'error',
{
required: {
some: ['nesting', 'id']
}
}
]
},
settings: {
'import/parsers': {

View File

@ -12,6 +12,7 @@
"@radix-ui/react-dropdown-menu": "^0.0.23",
"@radix-ui/react-polymorphic": "^0.0.13",
"@radix-ui/react-portal": "^0.0.15",
"@radix-ui/react-toggle": "^0.0.10",
"@urbit/api": "^1.4.0",
"@urbit/http-api": "^1.3.1",
"classnames": "^2.3.1",
@ -60,7 +61,8 @@
"tailwindcss": "^2.2.7",
"tailwindcss-touch": "^1.0.1",
"typescript": "^4.3.2",
"vite": "^2.4.4"
"vite": "^2.4.4",
"vite-plugin-html-config": "^1.0.5"
}
},
"node_modules/@babel/code-frame": {
@ -773,6 +775,21 @@
"@radix-ui/react-compose-refs": "0.0.5"
}
},
"node_modules/@radix-ui/react-toggle": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-0.0.10.tgz",
"integrity": "sha512-fr62h2p6tqayDBg3hkrdvVJFQQmzBNHolyOqF+MifeTAryBJ3AsQu9ZvuYxJzkIaPPAIGOohRdR48FSQGJWndA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.0.5",
"@radix-ui/react-polymorphic": "0.0.13",
"@radix-ui/react-primitive": "0.0.15",
"@radix-ui/react-use-controllable-state": "0.0.6"
},
"peerDependencies": {
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-use-body-pointer-events": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.7.tgz",
@ -6511,6 +6528,18 @@
"fsevents": "~2.3.2"
}
},
"node_modules/vite-plugin-html-config": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/vite-plugin-html-config/-/vite-plugin-html-config-1.0.5.tgz",
"integrity": "sha512-2v/nLbpFUofCsa19tw/ZcsqautjV2bBpYZH44ysxN2M1QXrnpYLgJhDUE918rKGsN0gTQ0Ej48luTaj5akTM7Q==",
"dev": true,
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"vite": "^2.0.5"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -7280,6 +7309,18 @@
"@radix-ui/react-compose-refs": "0.0.5"
}
},
"@radix-ui/react-toggle": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-0.0.10.tgz",
"integrity": "sha512-fr62h2p6tqayDBg3hkrdvVJFQQmzBNHolyOqF+MifeTAryBJ3AsQu9ZvuYxJzkIaPPAIGOohRdR48FSQGJWndA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.0.5",
"@radix-ui/react-polymorphic": "0.0.13",
"@radix-ui/react-primitive": "0.0.15",
"@radix-ui/react-use-controllable-state": "0.0.6"
}
},
"@radix-ui/react-use-body-pointer-events": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.7.tgz",
@ -11976,7 +12017,8 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/vite-plugin-html-config/-/vite-plugin-html-config-1.0.5.tgz",
"integrity": "sha512-2v/nLbpFUofCsa19tw/ZcsqautjV2bBpYZH44ysxN2M1QXrnpYLgJhDUE918rKGsN0gTQ0Ej48luTaj5akTM7Q==",
"dev": true
"dev": true,
"requires": {}
},
"which": {
"version": "2.0.2",

View File

@ -18,6 +18,7 @@
"@radix-ui/react-dropdown-menu": "^0.0.23",
"@radix-ui/react-polymorphic": "^0.0.13",
"@radix-ui/react-portal": "^0.0.15",
"@radix-ui/react-toggle": "^0.0.10",
"@urbit/api": "^1.4.0",
"@urbit/http-api": "^1.3.1",
"classnames": "^2.3.1",

View File

@ -0,0 +1,28 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<rect width="32" height="32" rx="8" fill="#219DFF" fill-opacity="0.3"/>
<g style="mix-blend-mode:multiply">
<path d="M40.5967 36.0162L19.8147 34.081C19.4445 34.0474 19.1416 33.7445 19.1248 33.3911L17.4084 -2.06444C17.3916 -2.41782 17.6944 -2.68706 18.0646 -2.65341L38.8466 -0.718245C39.2168 -0.68459 39.5197 -0.3817 39.5365 -0.0283222L41.2529 35.4272C41.2866 35.7806 40.9837 36.0498 40.5967 36.0162Z" fill="#E5E5E5"/>
<path d="M29.1031 12.1036C31.7113 11.9185 34.2186 14.1229 34.471 16.9499C34.7234 19.8106 32.5022 22.4525 29.7257 22.6545C26.966 22.8564 24.5092 20.6688 24.3072 17.825C24.0548 14.7624 26.091 12.3392 29.1031 12.1036Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M10.3238 26.5929C7.85016 26.7443 5.6121 24.8933 5.19141 22.3692C4.73707 19.6431 6.4703 16.9339 9.11222 16.3449C10.795 15.9747 12.3094 16.4459 13.5547 17.6575C14.9514 19.0037 15.574 20.6864 15.1701 22.6384C14.8167 24.2539 13.7398 25.3308 12.3263 26.0544C11.7036 26.3573 11.0306 26.5592 10.3238 26.5929Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M16.685 -4C16.8364 -3.71393 17.1057 -3.69711 17.3412 -3.66345C21.9856 -3.07449 26.63 -2.48553 31.2744 -1.89656C34.0004 -1.56001 36.7433 -1.22346 39.4694 -0.886914C39.6545 -0.870087 39.8396 -0.853251 40.0247 -0.836424C40.5295 -0.785941 40.8156 -0.499878 40.8324 0.0217743C40.8661 0.930459 40.8997 1.85597 40.9165 2.78149C41.0175 6.51719 41.3036 10.2361 41.5728 13.9549C41.7243 15.9574 41.7411 17.9935 41.8421 19.996C41.9935 23.0418 42.1618 26.1044 42.3301 29.1502C42.4479 31.1358 42.5825 33.1046 42.6834 35.0903C42.7171 35.6456 42.6834 36.2177 42.6498 36.773C42.633 37.1432 42.4815 37.261 42.1281 37.2273C40.2939 37.0422 38.4429 36.8572 36.6087 36.6889C34.8082 36.5374 32.9908 36.4196 31.1734 36.2682C29.2887 36.1167 27.4209 35.9485 25.553 35.7802C23.5506 35.6119 21.5649 35.4436 19.5625 35.2922C18.7547 35.2249 18.6538 35.1576 18.6706 34.2994C18.7043 31.994 18.4518 29.7055 18.3172 27.4001C18.2836 26.6765 18.2163 25.9698 18.1658 25.2462C18.149 25.0947 18.1321 24.9433 18.3172 24.9265C18.5023 24.9096 18.5528 25.0442 18.5696 25.2125C18.6201 25.9024 18.6538 26.5924 18.6874 27.2823C18.8221 29.655 18.9735 32.0108 19.1081 34.3835C19.125 34.7537 19.2091 34.8547 19.5793 34.8883C21.3125 35.0398 23.0289 35.1744 24.7622 35.309C27.4377 35.5278 30.1133 35.7634 32.7889 35.9821C35.0269 36.1672 37.265 36.3187 39.503 36.5038C40.2098 36.5543 40.9165 36.6216 41.6233 36.6721C42.1113 36.7057 42.1618 36.6889 42.145 36.1841C41.9935 33.0037 41.8589 29.8401 41.7074 26.6597C41.5896 24.2365 41.4719 21.8134 41.3372 19.3902C41.1858 16.4454 41.0343 13.5174 40.8661 10.5726C40.681 7.13979 40.4959 3.70699 40.3108 0.274187C40.2771 -0.331603 40.193 -0.449387 39.604 -0.516697C36.8106 -0.870075 34.0173 -1.22346 31.2239 -1.57684C28.4642 -1.93021 25.6877 -2.26676 22.928 -2.62013C21.0433 -2.85572 19.1754 -3.09132 17.2907 -3.3269C16.8869 -3.37738 16.7859 -3.29325 16.8196 -2.82208C16.9037 -1.32443 17.0047 0.190053 17.0888 1.6877C17.2739 4.78396 17.4759 7.89703 17.661 10.9933C17.7619 12.6424 17.8629 14.2747 17.9638 15.9238C17.9638 16.0079 17.8965 16.1088 17.8461 16.2098C17.7451 16.1593 17.5936 16.1425 17.56 16.0584C17.4927 15.9406 17.4758 15.7891 17.459 15.6377C17.3749 13.5174 17.3076 11.3972 17.1898 9.27689C17.0215 6.53401 16.8196 3.79113 16.6345 1.03142C16.5335 -0.398913 16.4494 -1.81243 16.3652 -3.24276C16.3821 -3.49518 16.4157 -3.78124 16.685 -4Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M2.85198 21.9824C1.96012 21.8815 1.1524 21.7805 0.327854 21.6964C-1.80924 21.4608 -3.94633 21.2084 -6.10025 20.9896C-8.84313 20.7035 -11.586 20.4175 -14.3457 20.1314C-14.4972 20.1146 -14.6654 20.1146 -14.8169 20.0641C-14.901 20.0304 -14.9683 19.9295 -15.0356 19.8622C-14.9515 19.8117 -14.8505 19.7275 -14.7664 19.7275C-14.1101 19.778 -13.4707 19.8453 -12.8312 19.9126C-10.1388 20.1987 -7.46327 20.5016 -4.77087 20.7877C-2.3982 21.0401 -0.042347 21.3093 2.33033 21.5617C2.48178 21.5786 2.63323 21.5617 2.78467 21.6122C2.86881 21.6291 2.93612 21.73 3.02025 21.7973C2.95294 21.8646 2.88563 21.9488 2.85198 21.9824Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M4.85503 13.7363C5.10744 13.8036 5.34303 13.8541 5.56179 13.9046C9.49942 14.8806 13.4539 15.8398 17.3915 16.8158C17.6944 16.8831 17.9973 16.9672 18.3002 17.0345C18.4348 17.0682 18.6031 17.1018 18.5358 17.2869C18.519 17.3542 18.317 17.4047 18.2329 17.3879C17.4252 17.2028 16.6006 17.0009 15.7929 16.7989C12.2086 15.9239 8.60756 15.032 5.02331 14.157C4.956 14.1402 4.88868 14.1402 4.8382 14.0897C4.77089 14.0392 4.7204 13.9551 4.66992 13.8878C4.7204 13.8373 4.78772 13.77 4.85503 13.7363Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M28.4973 9.64627C28.4468 9.71358 28.4132 9.81456 28.3459 9.84821C28.2954 9.86504 28.1944 9.81456 28.1439 9.76407C28.0766 9.69676 28.0261 9.5958 27.9756 9.51166C26.8145 7.03802 25.6534 4.54754 24.4924 2.0739C24.4419 1.97293 24.3746 1.88881 24.3409 1.77102C24.3241 1.70371 24.3577 1.56908 24.3914 1.53543C24.4587 1.50177 24.5597 1.53542 24.6438 1.56907C24.6943 1.5859 24.7111 1.67004 24.7279 1.72052C25.9227 4.24465 27.1174 6.76877 28.3122 9.29289C28.3795 9.39386 28.43 9.51165 28.4973 9.64627Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M17.5598 22.2002C18.4012 22.2843 19.1584 22.3685 19.9325 22.4526C20.7907 22.5367 21.6657 22.6377 22.5239 22.7387C22.6417 22.7555 22.81 22.7387 22.7932 22.9406C22.7763 23.1257 22.6585 23.1425 22.4903 23.1257C20.8748 22.9574 19.2762 22.806 17.6608 22.6545C17.6608 22.6545 17.6439 22.6545 17.6271 22.6545C17.4925 22.6377 17.3242 22.604 17.3579 22.4189C17.3747 22.318 17.5093 22.2507 17.5598 22.2002Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M22.9613 14.7636C22.8435 14.7131 22.7593 14.6795 22.692 14.629C22.1704 14.2924 21.6655 13.9391 21.1439 13.6025C20.6222 13.266 20.0838 12.9462 19.5621 12.6265C19.5116 12.5929 19.4275 12.5592 19.4107 12.5087C19.377 12.4414 19.3602 12.3405 19.3938 12.2731C19.4107 12.2395 19.5285 12.2227 19.5958 12.2227C19.6463 12.2227 19.6799 12.2731 19.7304 12.29C20.8073 12.9631 21.8675 13.6193 22.9444 14.2924C22.9949 14.3261 23.0454 14.3598 23.0959 14.3934C23.1464 14.4607 23.2305 14.5617 23.2137 14.5953C23.1295 14.6626 23.0286 14.7131 22.9613 14.7636Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M21.6151 3.20215C21.6487 3.2358 21.7329 3.28629 21.7833 3.3536C22.4396 4.41373 23.0959 5.49069 23.7522 6.56765C23.7858 6.63496 23.7522 6.76957 23.7353 6.85371C23.6512 6.82005 23.5166 6.80323 23.4661 6.73592C22.7762 5.67579 22.1031 4.61566 21.43 3.5387C21.3458 3.38725 21.4131 3.21898 21.6151 3.20215Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M24.3413 8.09879C24.375 8.04831 24.3918 7.94734 24.4254 7.93051C24.4928 7.89686 24.5937 7.89686 24.6274 7.93051C24.7115 7.99782 24.7788 8.09879 24.8461 8.18293C25.3846 9.07478 25.9231 9.94982 26.4784 10.8417C26.5625 10.9595 26.613 11.0941 26.4616 11.1614C26.3943 11.195 26.2428 11.1277 26.1923 11.0436C25.6202 10.1517 25.0649 9.25988 24.5096 8.36803C24.4254 8.30072 24.3918 8.19975 24.3413 8.09879Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M19.3768 17.3047C19.7638 17.4057 20.1172 17.473 20.4706 17.5739C21.1774 17.7422 21.9009 17.9273 22.6077 18.0956C22.6582 18.1124 22.7423 18.1124 22.776 18.1461C22.8433 18.2134 22.8938 18.2975 22.9442 18.3648C22.8433 18.4153 22.7423 18.5163 22.675 18.4995C22.1029 18.3648 21.5476 18.2134 20.9754 18.0788C20.4538 17.9441 19.949 17.8095 19.4273 17.6749C19.3432 17.6581 19.2758 17.5571 19.2422 17.473C19.2422 17.4393 19.3432 17.3552 19.3768 17.3047Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M0.58035 17.1855C0.98421 17.2697 1.33759 17.337 1.69097 17.4043C2.31359 17.5221 2.91938 17.6399 3.54199 17.7409C3.67661 17.7577 3.81123 17.7913 3.77757 17.9764C3.74392 18.1447 3.6093 18.1447 3.47468 18.1279C2.49869 17.9428 1.53952 17.7577 0.563527 17.5557C0.496217 17.5389 0.445728 17.4211 0.378418 17.3538C0.445728 17.3033 0.51304 17.236 0.58035 17.1855Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M20.6391 20.8874C20.4372 21.1903 20.2184 21.2239 19.9155 21.1566C19.3938 21.022 18.8554 20.9379 18.3337 20.8032C18.2496 20.7864 18.1655 20.6686 18.0981 20.6013C18.1991 20.5508 18.3169 20.433 18.401 20.4499C19.1414 20.5845 19.8819 20.7359 20.6391 20.8874Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M30.8868 8.97362C30.8532 9.0241 30.8027 9.09142 30.7354 9.17556C30.6512 9.10825 30.5334 9.04093 30.5166 8.95679C30.382 8.30052 30.2474 7.66108 30.1464 7.00481C30.1296 6.92067 30.2305 6.81971 30.281 6.71875C30.3483 6.78606 30.4661 6.83654 30.483 6.90385C30.6176 7.57695 30.7522 8.25004 30.8868 8.97362Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M23.0117 21.5446C22.8098 21.5615 22.6079 21.5951 22.4228 21.5783C22.0694 21.5278 21.7329 21.4605 21.3795 21.3764C21.3122 21.3595 21.228 21.2586 21.2449 21.2081C21.2449 21.1576 21.3458 21.0566 21.3963 21.0566C21.9516 21.0903 22.5069 21.1913 23.0117 21.4605C23.0286 21.4942 23.0117 21.511 23.0117 21.5446Z" fill="black" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="32" height="32" rx="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Docket } from '@urbit/api/docket';
import {DocketImage} from './DocketImage';
import { DocketImage } from './DocketImage';
interface DocketHeaderProps {
docket: Docket;

View File

@ -0,0 +1,36 @@
import classNames from 'classnames';
import React, { FC, HTMLAttributes } from 'react';
import slugify from 'slugify';
import { useAsyncCall } from '../logic/useAsyncCall';
import { Spinner } from './Spinner';
import { Toggle } from './Toggle';
type SettingsProps = {
name: string;
on: boolean;
toggle: (open: boolean) => Promise<void>;
} & HTMLAttributes<HTMLDivElement>;
export const Setting: FC<SettingsProps> = ({ name, on, toggle, className, children }) => {
const { status, call } = useAsyncCall(toggle);
const id = slugify(name);
return (
<section className={classNames('inner-section', className)}>
<h3 id={id} className="flex items-center h4 mb-2">
{name} {status === 'loading' && <Spinner className="ml-2" />}
</h3>
<div className="flex">
<div className="flex-none mr-2">
<Toggle
aria-labelledby={id}
pressed={on}
onPressedChange={call}
className="text-blue-400"
/>
</div>
<div className="flex-1 space-y-6">{children}</div>
</div>
</section>
);
};

View File

@ -0,0 +1,55 @@
import classNames from 'classnames';
import React, { useState } from 'react';
import * as RadixToggle from '@radix-ui/react-toggle';
import type * as Polymorphic from '@radix-ui/react-polymorphic';
type ToggleComponent = Polymorphic.ForwardRefComponent<
Polymorphic.IntrinsicElement<typeof RadixToggle.Root>,
Polymorphic.OwnProps<typeof RadixToggle.Root> & {
knobClass?: string;
}
>;
export const Toggle = React.forwardRef(
({ defaultPressed, pressed, onPressedChange, disabled, className }, ref) => {
const [on, setOn] = useState(defaultPressed);
const isControlled = !!onPressedChange;
const proxyPressed = isControlled ? pressed : on;
const proxyOnPressedChange = isControlled ? onPressedChange : setOn;
const knobPosition = proxyPressed ? 18 : 2;
return (
<RadixToggle.Root
className="default-ring rounded-full"
pressed={proxyPressed}
onPressedChange={proxyOnPressedChange}
disabled={disabled}
ref={ref}
>
<svg
className={classNames('w-12 h-8', className)}
viewBox="0 0 48 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
className={classNames(
'fill-current',
disabled && proxyPressed && 'text-gray-700',
!proxyPressed && 'text-gray-200'
)}
d="M0 16C0 7.16344 7.16344 0 16 0H32C40.8366 0 48 7.16344 48 16C48 24.8366 40.8366 32 32 32H16C7.16344 32 0 24.8366 0 16Z"
/>
<rect
className={classNames('fill-current text-white', disabled && 'opacity-60')}
x={knobPosition}
y="2"
width="28"
height="28"
rx="14"
/>
</svg>
</RadixToggle.Root>
);
}
) as ToggleComponent;

View File

@ -0,0 +1,30 @@
import { useCallback, useState } from 'react';
export type Status = 'initial' | 'loading' | 'success' | 'error';
export function useAsyncCall<ReturnValue>(cb: (...args: any[]) => Promise<ReturnValue>) {
const [status, setStatus] = useState<Status>('initial');
const [error, setError] = useState<Error | null>(null);
const call = useCallback(
(...args: any[]) => {
setStatus('loading');
cb(...args)
.then((result) => {
setStatus('success');
return result;
})
.catch((err) => {
setError(err);
setStatus('error');
});
},
[cb]
);
return {
call,
status,
error
};
}

View File

@ -0,0 +1,30 @@
import { debounce, DebounceSettings } from 'lodash-es';
import { useRef, useEffect, useCallback } from 'react';
import { useIsMounted } from './useIsMounted';
export function useDebounce(
cb: (...args: any[]) => void,
delay: number,
options?: DebounceSettings
) {
const isMounted = useIsMounted();
const inputsRef = useRef({ cb, delay }); // mutable ref like with useThrottle
useEffect(() => {
inputsRef.current = { cb, delay };
}); // also track cur. delay
return useCallback(
debounce(
(...args) => {
// Debounce is an async callback. Cancel it, if in the meanwhile
// (1) component has been unmounted (see isMounted in snippet)
// (2) delay has changed
if (inputsRef.current.delay === delay && isMounted()) inputsRef.current.cb(...args);
},
delay,
options
),
[delay, debounce]
);
}

View File

@ -0,0 +1,11 @@
import { useRef, useEffect } from 'react';
export function useIsMounted() {
const isMountedRef = useRef(true);
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
return () => isMountedRef.current;
}

View File

@ -1,5 +1,4 @@
import classNames from 'classnames';
import { debounce } from 'lodash-es';
import React, {
ChangeEvent,
FocusEvent,
@ -12,6 +11,7 @@ import React, {
} from 'react';
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
import { Cross } from '../components/icons/Cross';
import { useDebounce } from '../logic/useDebounce';
import { MenuState, useLeapStore } from './Nav';
function normalizePathEnding(path: string) {
@ -87,22 +87,21 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
[menu]
);
const handleSearch = useCallback(
debounce(
(input: string) => {
if (!match || appsMatch) {
return;
}
const debouncedSearch = useDebounce(
(input: string) => {
if (!match || appsMatch) {
return;
}
useLeapStore.setState({ searchInput: input });
navigateByInput(input);
},
300,
{ leading: true }
),
[menu, match]
useLeapStore.setState({ searchInput: input });
navigateByInput(input);
},
300,
{ leading: true }
);
const handleSearch = useCallback(debouncedSearch, [match]);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const input = e.target as HTMLInputElement;

View File

@ -133,9 +133,10 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
return (
<>
{/* Using portal so that we can retain the same nav items both in the dialog and in the base header */}
<Portal.Root
containerRef={dialogContentOpen ? dialogNavRef : navRef}
className="flex space-x-2"
className="flex justify-center w-full space-x-2"
>
<SystemMenu
open={systemMenuOpen}
@ -149,7 +150,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
menu={menuState}
dropdown="leap-items"
showClose={isOpen}
className={!isOpen ? 'bg-gray-100' : ''}
className={classNames('flex-1 max-w-[600px]', !isOpen ? 'bg-gray-100' : '')}
/>
</Portal.Root>
<div

View File

@ -1,20 +1,72 @@
import React, { useEffect } from 'react';
import React, { useCallback, useEffect } from 'react';
import { Link, Route, RouteComponentProps, Switch, useRouteMatch } from 'react-router-dom';
import classNames from 'classnames';
import { useLeapStore } from './Nav';
import { NotificationPrefs } from './preferences/NotificationPrefs';
import { SystemUpdatePrefs } from './preferences/SystemUpdatePrefs';
import notificationsSVG from '../assets/notifications.svg';
import systemUpdatesSVG from '../assets/system-updates.svg';
export const SystemPreferences = () => {
export const SystemPreferences = ({ match }: RouteComponentProps<{ submenu: string }>) => {
const select = useLeapStore((state) => state.select);
const subMatch = useRouteMatch<{ submenu: string }>(`${match.url}/:submenu`);
useEffect(() => {
select('System Preferences');
}, []);
const matchSub = useCallback(
(target: string) => {
if (!subMatch && target === 'notifications') {
return true;
}
return subMatch?.params.submenu === target;
},
[match, subMatch]
);
return (
<div className="p-4 md:p-8 space-y-8">
<h2 className="h4 text-gray-500">Recent Apps</h2>
<div className="min-h-[150px] rounded-xl bg-gray-100" />
<hr className="-mx-4 md:-mx-8" />
<h2 className="h4 text-gray-500">Recent Developers</h2>
<div className="min-h-[150px] rounded-xl bg-gray-100" />
<div className="flex h-[600px] max-h-full">
<aside className="flex-none min-w-60 border-r-2 border-gray-100">
<div className="p-5">
<input className="input h4 default-ring bg-gray-100" placeholder="Search Preferences" />
</div>
<nav className="border-b-2 border-gray-100">
<ul className="font-semibold">
<li>
<Link
to={`${match.url}/notifications`}
className={classNames(
'flex items-center px-5 py-3 hover:text-black hover:bg-gray-100',
matchSub('notifications') && 'text-black bg-gray-100'
)}
>
<img className="w-8 h-8 mr-3" src={notificationsSVG} alt="" />
Notifications
</Link>
</li>
<li>
<Link
to={`${match.url}/system-updates`}
className={classNames(
'flex items-center px-5 py-3 hover:text-black hover:bg-gray-100',
matchSub('system-updates') && 'text-black bg-gray-100'
)}
>
<img className="w-8 h-8 mr-3" src={systemUpdatesSVG} alt="" />
System Updates
</Link>
</li>
</ul>
</nav>
</aside>
<section className="flex-1 px-5 py-7 text-black">
<Switch>
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
<Route path={[`${match.url}/notifications`, match.url]} component={NotificationPrefs} />
</Switch>
</section>
</div>
);
};

View File

@ -0,0 +1,41 @@
import React, { useEffect } from 'react';
import { Setting } from '../../components/Setting';
import { useLeapStore } from '../Nav';
import { usePreferencesStore } from './usePreferencesStore';
export const NotificationPrefs = () => {
const select = useLeapStore((s) => s.select);
const { doNotDisturb, mentions, toggleDoNotDisturb, toggleMentions } = usePreferencesStore();
useEffect(() => {
select('System Preferences: Notifications');
}, []);
return (
<>
<h2 className="h3 mb-7">Notifications</h2>
<div className="space-y-3">
<Setting on={doNotDisturb} toggle={toggleDoNotDisturb} name="Do Not Disturb">
<p>
Block visual desktop notifications whenever Urbit software produces an in-Landscape
notification badge.
</p>
<p>
Turning this &quot;off&quot; will prompt your browser to ask if you&apos;d like to
enable notifications
</p>
</Setting>
<Setting on={mentions} toggle={toggleMentions} name="Mentions">
<p>
[PLACEHOLDER] Block visual desktop notifications whenever Urbit software produces an
in-Landscape notification badge.
</p>
<p>
Turning this &quot;off&quot; will prompt your browser to ask if you&apos;d like to
enable notifications
</p>
</Setting>
</div>
</>
);
};

View File

@ -0,0 +1,81 @@
import React, { ChangeEvent, FormEvent, useCallback, useEffect, useState } from 'react';
import { Button } from '../../components/Button';
import { Setting } from '../../components/Setting';
import { ShipName } from '../../components/ShipName';
import { Spinner } from '../../components/Spinner';
import { useAsyncCall } from '../../logic/useAsyncCall';
import { useLeapStore } from '../Nav';
import { usePreferencesStore } from './usePreferencesStore';
export const SystemUpdatePrefs = () => {
const select = useLeapStore((s) => s.select);
const { otasEnabled, otaSource, toggleOTAs, setOTASource } = usePreferencesStore();
const [source, setSource] = useState(otaSource);
const sourceDirty = source !== otaSource;
const { status: sourceStatus, call: setOTA } = useAsyncCall(setOTASource);
useEffect(() => {
select('System Preferences: Updates');
}, []);
useEffect(() => {
setSource(otaSource);
}, [otaSource]);
const handleSourceChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const { target } = e;
const value = target.value.trim();
setSource(value.startsWith('~') ? value : `~${value}`);
}, []);
const onSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setOTA(source);
},
[source]
);
return (
<>
<h2 className="h3 mb-7">System Updates</h2>
<div className="space-y-3">
<Setting on={otasEnabled} toggle={toggleOTAs} name="Enable Automatic Urbit OTAs">
<p>Automatically download and apply system updates to keep your Urbit up to date.</p>
<p>
OTA Source: <ShipName name={otaSource} className="font-semibold font-mono" />
</p>
</Setting>
<form className="inner-section relative" onSubmit={onSubmit}>
<label htmlFor="ota-source" className="h4 mb-3">
Switch OTA Source
</label>
<p className="mb-2">
Enter a valid urbit name into this form to change who you receive OTA updates from. Be
sure to select a reliable urbit!
</p>
<div className="relative">
<input
id="ota-source"
type="text"
value={source}
onChange={handleSourceChange}
className="input font-semibold default-ring"
/>
{sourceDirty && (
<Button type="submit" className="absolute top-1 right-1 py-1 px-3 text-sm">
{sourceStatus !== 'loading' && 'Save'}
{sourceStatus === 'loading' && (
<>
<span className="sr-only">Saving...</span>
<Spinner className="w-5 h-5" />
</>
)}
</Button>
)}
</div>
</form>
</div>
</>
);
};

View File

@ -0,0 +1,51 @@
import create from 'zustand';
import { fakeRequest } from '../../state/util';
const useMockData = import.meta.env.MODE === 'mock';
interface PreferencesStore {
otasEnabled: boolean;
otaSource: string;
doNotDisturb: boolean;
mentions: boolean;
setOTASource: (source: string) => Promise<void>;
toggleOTAs: () => Promise<void>;
toggleDoNotDisturb: () => Promise<void>;
toggleMentions: () => Promise<void>;
}
export const usePreferencesStore = create<PreferencesStore>((set) => ({
otasEnabled: true,
otaSource: useMockData ? '~sabbus' : '',
doNotDisturb: false,
mentions: true,
/**
* a lot of these are repetitive, we may do better with a map of settings
* and some generic way to update them through pokes. That way, we could
* just have toggleSetting(key) and run a similar op for all
*/
toggleOTAs: async () => {
if (useMockData) {
await fakeRequest();
set((state) => ({ otasEnabled: !state.otasEnabled }));
}
},
setOTASource: async (source: string) => {
if (useMockData) {
await fakeRequest();
set({ otaSource: source });
}
},
toggleDoNotDisturb: async () => {
if (useMockData) {
await fakeRequest();
set((state) => ({ doNotDisturb: !state.doNotDisturb }));
}
},
toggleMentions: async () => {
if (useMockData) {
await fakeRequest();
set((state) => ({ mentions: !state.mentions }));
}
}
}));

View File

@ -29,7 +29,5 @@ export const TreatyInfo = () => {
</div>
);
}
return (
<AppInfo className="dialog-inner-container" docket={!!charge ? charge : treaty} vat={vat} />
);
return <AppInfo className="dialog-inner-container" docket={charge || treaty} vat={vat} />;
};

View File

@ -8,7 +8,7 @@ export function makeKeyFn(key: string) {
export const useMockData = import.meta.env.MODE === 'mock';
export async function fakeRequest<T>(data: T, time = 300): Promise<T> {
export async function fakeRequest<T>(data?: any, time = 300): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(data);

View File

@ -22,6 +22,14 @@
@apply min-w-52 p-4 rounded-xl;
}
.inner-section {
@apply p-3 bg-gray-100 rounded-xl;
}
.input {
@apply px-4 py-2 w-full bg-white rounded-xl;
}
.notification {
@apply p-4 bg-gray-100 rounded-xl;
}