mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-01 19:46:36 +03:00
Merge remote-tracking branch 'origin/dist' into lf/app-linking
This commit is contained in:
commit
00d08a4543
2
.gitignore
vendored
2
.gitignore
vendored
@ -78,3 +78,5 @@ pkg/interface/link-webext/web-ext-artifacts
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
.vercel
|
||||
|
@ -415,14 +415,15 @@
|
||||
?: =(`[her rem] got)
|
||||
~> %slog.0^leaf/"kiln: already tracking {here:(abed lac)}, ignoring"
|
||||
vats
|
||||
=? kiln ?=(^ got) (uninstall lac)
|
||||
=: loc lac
|
||||
rak [[paused=| her rem *aeon] next=~ *rein]
|
||||
rak [[paused=| her rem *aeon] next=~ rein:(fall got *arak)]
|
||||
==
|
||||
~> %slog.0^leaf/"kiln: beginning install into {here}"
|
||||
(emit find:pass)
|
||||
:: +reset: resync after failure
|
||||
::
|
||||
:: TODO: don't blow away so much state
|
||||
::
|
||||
++ reset
|
||||
^+ vats
|
||||
~> %slog.0^leaf/"kiln: resetting tracking for {here}"
|
||||
@ -445,6 +446,8 @@
|
||||
vats
|
||||
:: +resume: restart tracking from upstream
|
||||
::
|
||||
:: TODO: check whether kelvin is legit
|
||||
::
|
||||
++ resume
|
||||
|= lac=desk
|
||||
^+ vats
|
||||
@ -528,7 +531,16 @@
|
||||
++ bump-many
|
||||
|= [kel=weft live=(set desk)]
|
||||
^+ kiln
|
||||
=/ liv ~(tap in live)
|
||||
:: ensure %base is always reloaded first
|
||||
::
|
||||
=/ liv
|
||||
%+ sort ~(tap in live)
|
||||
|= [a=desk b=desk]
|
||||
^- ?
|
||||
?: =(%base a) &
|
||||
?: =(%base b) |
|
||||
(lte `@`a `@`b)
|
||||
::
|
||||
|- ^+ kiln
|
||||
?~ liv kiln
|
||||
$(liv t.liv, kiln (bump-one kel i.liv))
|
||||
@ -691,7 +703,7 @@
|
||||
(update-running-apps (get-apps-diff our loc now rein.rak))
|
||||
?. =(%base loc)
|
||||
vats
|
||||
~> %slog.0^leaf/"kiln: bumping {<zuse>}"
|
||||
~> %slog.0^leaf/"kiln: bumping {<zuse>}" :: TODO print next
|
||||
(emit merge-kids:pass)
|
||||
::
|
||||
++ take-merge-kids
|
||||
|
@ -2,6 +2,7 @@
|
||||
+$ bump [%kiln-bump except=(set desk) force=_|]
|
||||
--
|
||||
|_ b=bump
|
||||
++ grad %noun
|
||||
++ grab
|
||||
|%
|
||||
++ noun bump
|
||||
|
@ -1,5 +1,6 @@
|
||||
/- *hood
|
||||
|_ vats=(list vat)
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun vats
|
||||
|
@ -70,16 +70,18 @@
|
||||
leaf/"pending: {<(turn next.arak |=([@ lal=@tas num=@] [lal num]))>}"
|
||||
^- tang
|
||||
=/ meb (mergebase-hashes our desk now arak)
|
||||
=/ poz ?:(paused.rail.arak "paused" "tracking")
|
||||
=/ sat ?:(liv.rein.arak "running" "suspended")
|
||||
:~ leaf/"/sys/kelvin: {<[lal num]:weft>}"
|
||||
leaf/"base hash: {?.(=(1 (lent meb)) <meb> <(head meb)>)}"
|
||||
leaf/"%cz hash: {<hash>}"
|
||||
leaf/"source ship: {<ship.rail.arak>}"
|
||||
leaf/"source desk: {<desk.rail.arak>}"
|
||||
leaf/"source aeon: {<aeon.rail.arak>}"
|
||||
leaf/"status: {sat}"
|
||||
leaf/"force on: {?:(=(~ add.rein.arak) "~" <add.rein.arak>)}"
|
||||
leaf/"force off: {?:(=(~ sub.rein.arak) "~" <sub.rein.arak>)}"
|
||||
:~ leaf/"/sys/kelvin: {<[lal num]:weft>}"
|
||||
leaf/"base hash: {?.(=(1 (lent meb)) <meb> <(head meb)>)}"
|
||||
leaf/"%cz hash: {<hash>}"
|
||||
leaf/"updates: {sat}"
|
||||
leaf/"source ship: {<ship.rail.arak>}"
|
||||
leaf/"source desk: {<desk.rail.arak>}"
|
||||
leaf/"source aeon: {<aeon.rail.arak>}"
|
||||
leaf/"agent status: {sat}"
|
||||
leaf/"force on: {?:(=(~ add.rein.arak) "~" <add.rein.arak>)}"
|
||||
leaf/"force off: {?:(=(~ sub.rein.arak) "~" <sub.rein.arak>)}"
|
||||
==
|
||||
:: +read-kelvin-foreign: read /sys/kelvin from a foreign desk
|
||||
::
|
||||
|
13693
pkg/btc-wallet/package-lock.json
generated
13693
pkg/btc-wallet/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
pkg/garden-dev/sur/settings.hoon
Normal file
44
pkg/garden-dev/sur/settings.hoon
Normal file
@ -0,0 +1,44 @@
|
||||
/+ *mip
|
||||
|%
|
||||
::
|
||||
++ settings-0
|
||||
=< settings
|
||||
|%
|
||||
+$ settings (map key bucket)
|
||||
+$ bucket (map key val)
|
||||
+$ val
|
||||
$% [%s p=@t]
|
||||
[%b p=?]
|
||||
[%n p=@]
|
||||
==
|
||||
--
|
||||
::
|
||||
++ settings-1
|
||||
=< settings
|
||||
|%
|
||||
+$ settings (map key bucket)
|
||||
--
|
||||
+$ bucket (map key val)
|
||||
+$ key term
|
||||
+$ val
|
||||
$~ [%n 0]
|
||||
$% [%s p=@t]
|
||||
[%b p=?]
|
||||
[%n p=@]
|
||||
[%a p=(list val)]
|
||||
==
|
||||
::
|
||||
+$ settings (mip desk key bucket)
|
||||
+$ event
|
||||
$% [%put-bucket =desk =key =bucket]
|
||||
[%del-bucket =desk =key]
|
||||
[%put-entry =desk buc=key =key =val]
|
||||
[%del-entry =desk buc=key =key]
|
||||
==
|
||||
+$ data
|
||||
$% [%all =settings]
|
||||
[%bucket =bucket]
|
||||
[%desk desk=(map key bucket)]
|
||||
[%entry =val]
|
||||
==
|
||||
--
|
@ -5,11 +5,13 @@
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
state-1
|
||||
state-2
|
||||
==
|
||||
+$ state-0 [%0 settings=settings-0]
|
||||
+$ state-1 [%1 =settings]
|
||||
+$ state-1 [%1 settings=settings-1]
|
||||
+$ state-2 [%2 =settings]
|
||||
--
|
||||
=| state-1
|
||||
=| state-2
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
@ -25,9 +27,8 @@
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
=^ cards state
|
||||
(put-entry:do %tutorial %seen b+|)
|
||||
(put-entry:do q.byk.bol %tutorial %seen b+|)
|
||||
[cards this]
|
||||
|
||||
::
|
||||
++ on-save !>(state)
|
||||
::
|
||||
@ -38,7 +39,8 @@
|
||||
|-
|
||||
?- -.old
|
||||
%0 $(old [%1 +.old])
|
||||
%1 [~ this(state old)]
|
||||
%1 $(old [%2 (~(put by *^settings) q.byk.bol settings.old)])
|
||||
%2 `this(state old)
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
@ -50,10 +52,10 @@
|
||||
=/ evt=event !<(event vas)
|
||||
=^ cards state
|
||||
?- -.evt
|
||||
%put-bucket (put-bucket:do key.evt bucket.evt)
|
||||
%del-bucket (del-bucket:do key.evt)
|
||||
%put-entry (put-entry:do buc.evt key.evt val.evt)
|
||||
%del-entry (del-entry:do buc.evt key.evt)
|
||||
%put-bucket (put-bucket:do [desk key bucket]:evt)
|
||||
%del-bucket (del-bucket:do [desk key]:evt)
|
||||
%put-entry (put-entry:do [desk buc key val]:evt)
|
||||
%del-entry (del-entry:do [desk buc key]:evt)
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
@ -65,15 +67,22 @@
|
||||
[%all ~]
|
||||
[~ this]
|
||||
::
|
||||
[%bucket @ ~]
|
||||
=* bucket-key i.t.pax
|
||||
?> (~(has by settings) bucket-key)
|
||||
[%desk @ ~]
|
||||
=* desk i.t.pax
|
||||
?> (~(has by settings) desk)
|
||||
[~ this]
|
||||
::
|
||||
[%entry @ @ ~]
|
||||
=* bucket-key i.t.pax
|
||||
=* entry-key i.t.t.pax
|
||||
=/ bucket (~(got by settings) bucket-key)
|
||||
[%bucket @ @ ~]
|
||||
=* desk i.t.pax
|
||||
=* bucket-key i.t.t.pax
|
||||
?> (~(has bi settings) desk bucket-key)
|
||||
[~ this]
|
||||
::
|
||||
[%entry @ @ @ ~]
|
||||
=* desk i.t.pax
|
||||
=* bucket-key i.t.t.pax
|
||||
=* entry-key i.t.t.t.pax
|
||||
=/ bucket (~(got bi settings) desk bucket-key)
|
||||
?> (~(has by bucket) entry-key)
|
||||
[~ this]
|
||||
==
|
||||
@ -85,29 +94,38 @@
|
||||
[%x %all ~]
|
||||
``settings-data+!>(`data`all+settings)
|
||||
::
|
||||
[%x %bucket @ ~]
|
||||
=* buc i.t.t.pax
|
||||
=/ bucket=(unit bucket) (~(get by settings) buc)
|
||||
[%x %desk @ ~]
|
||||
=* desk i.t.t.pax
|
||||
?~ desk-settings=(~(get by settings) desk) [~ ~]
|
||||
``settings-data+!>(desk+u.desk-settings)
|
||||
::
|
||||
[%x %bucket @ @ ~]
|
||||
=* desk i.t.t.pax
|
||||
=* buc i.t.t.t.pax
|
||||
=/ bucket=(unit bucket) (~(get bi settings) desk buc)
|
||||
?~ bucket [~ ~]
|
||||
``settings-data+!>(`data`bucket+u.bucket)
|
||||
::
|
||||
[%x %entry @ @ ~]
|
||||
=* buc i.t.t.pax
|
||||
=* key i.t.t.t.pax
|
||||
=/ =bucket (fall (~(get by settings) buc) ~)
|
||||
[%x %entry @ @ @ ~]
|
||||
=* desk i.t.t.pax
|
||||
=* buc i.t.t.t.pax
|
||||
=* key i.t.t.t.t.pax
|
||||
=/ =bucket (~(gut bi settings) desk buc *bucket)
|
||||
=/ entry=(unit val) (~(get by bucket) key)
|
||||
?~ entry [~ ~]
|
||||
``settings-data+!>(`data`entry+u.entry)
|
||||
::
|
||||
[%x %has-bucket @ ~]
|
||||
=* buc i.t.t.pax
|
||||
=/ has-bucket=? (~(has by settings) buc)
|
||||
[%x %has-bucket @ @ ~]
|
||||
=/ desk i.t.t.pax
|
||||
=/ buc i.t.t.t.pax
|
||||
=/ has-bucket=? (~(has bi settings) desk buc)
|
||||
``noun+!>(`?`has-bucket)
|
||||
::
|
||||
[%x %has-entry @ @ ~]
|
||||
=* buc i.t.t.pax
|
||||
=* key i.t.t.t.pax
|
||||
=/ =bucket (fall (~(get by settings) buc) ~)
|
||||
[%x %has-entry @ @ @ ~]
|
||||
=* desk i.t.t.pax
|
||||
=* buc i.t.t.t.pax
|
||||
=* key i.t.t.t.t.pax
|
||||
=/ =bucket (~(gut bi settings) desk buc *bucket)
|
||||
=/ has-entry=? (~(has by bucket) key)
|
||||
``noun+!>(`?`has-entry)
|
||||
==
|
||||
@ -124,60 +142,63 @@
|
||||
:: already exists
|
||||
::
|
||||
++ put-bucket
|
||||
|= [=key =bucket]
|
||||
|= [=desk =key =bucket]
|
||||
^- (quip card _state)
|
||||
=/ pas=(list path)
|
||||
:~ /all
|
||||
/bucket/[key]
|
||||
/desk/[desk]
|
||||
/bucket/[desk]/[key]
|
||||
==
|
||||
:- [(give-event pas %put-bucket key bucket)]~
|
||||
state(settings (~(put by settings) key bucket))
|
||||
:- [(give-event pas %put-bucket desk key bucket)]~
|
||||
state(settings (~(put bi settings) desk key bucket))
|
||||
::
|
||||
:: +del-bucket: delete a bucket from the top level settings map
|
||||
::
|
||||
++ del-bucket
|
||||
|= =key
|
||||
|= [=desk =key]
|
||||
^- (quip card _state)
|
||||
=/ pas=(list path)
|
||||
:~ /all
|
||||
/desk/[desk]
|
||||
/bucket/[key]
|
||||
==
|
||||
:- [(give-event pas %del-bucket key)]~
|
||||
state(settings (~(del by settings) key))
|
||||
:- [(give-event pas %del-bucket desk key)]~
|
||||
state(settings (~(del bi settings) desk key))
|
||||
::
|
||||
:: +put-entry: put an entry in a bucket, overwriting if it already exists
|
||||
:: if bucket does not yet exist, create it
|
||||
::
|
||||
++ put-entry
|
||||
|= [buc=key =key =val]
|
||||
|= [=desk buc=key =key =val]
|
||||
^- (quip card _state)
|
||||
=/ pas=(list path)
|
||||
:~ /all
|
||||
/bucket/[buc]
|
||||
/entry/[buc]/[key]
|
||||
/desk/[desk]
|
||||
/bucket/[desk]/[buc]
|
||||
/entry/[desk]/[buc]/[key]
|
||||
==
|
||||
=/ =bucket (fall (~(get by settings) buc) ~)
|
||||
=. bucket (~(put by bucket) key val)
|
||||
:- [(give-event pas %put-entry buc key val)]~
|
||||
state(settings (~(put by settings) buc bucket))
|
||||
=/ =bucket (~(put by (~(gut bi settings) desk buc *bucket)) key val)
|
||||
:- [(give-event pas %put-entry desk buc key val)]~
|
||||
state(settings (~(put bi settings) desk key bucket))
|
||||
::
|
||||
:: +del-entry: delete an entry from a bucket, fail quietly if bucket does not
|
||||
:: exist
|
||||
::
|
||||
++ del-entry
|
||||
|= [buc=key =key]
|
||||
|= [=desk buc=key =key]
|
||||
^- (quip card _state)
|
||||
=/ pas=(list path)
|
||||
:~ /all
|
||||
/bucket/[buc]
|
||||
/entry/[buc]/[key]
|
||||
/desk/[desk]
|
||||
/bucket/[desk]/[buc]
|
||||
/entry/[desk]/[buc]/[key]
|
||||
==
|
||||
=/ bucket=(unit bucket) (~(get by settings) buc)
|
||||
=/ bucket=(unit bucket) (~(get bi settings) desk buc)
|
||||
?~ bucket
|
||||
[~ state]
|
||||
=. u.bucket (~(del by u.bucket) key)
|
||||
:- [(give-event pas %del-entry buc key)]~
|
||||
state(settings (~(put by settings) buc u.bucket))
|
||||
:- [(give-event pas %del-entry desk buc key)]~
|
||||
state(settings (~(put bi settings) desk buc u.bucket))
|
||||
::
|
||||
++ give-event
|
||||
|= [pas=(list path) evt=event]
|
@ -1,6 +1,7 @@
|
||||
:~ :- %apes
|
||||
:~ %docket
|
||||
%treaty
|
||||
%settings-store
|
||||
==
|
||||
:- %fish ~
|
||||
==
|
||||
|
@ -1,8 +1,8 @@
|
||||
:~ title+'Garden'
|
||||
info+'An app launcher for Urbit.'
|
||||
color+0xee.5432
|
||||
::glob-http+'https://bootstrap.urbit.org/glob-0v6.t43bu.cpl0b.bsisc.sqr4d.dckpn.glob'
|
||||
glob-ames+~zod
|
||||
glob-http+'https://bootstrap.urbit.org/glob-0v6.t43bu.cpl0b.bsisc.sqr4d.dckpn.glob'
|
||||
::glob-ames+~zod
|
||||
base+'grid'
|
||||
version+[0 0 1]
|
||||
website+'https://tlon.io'
|
||||
|
55
pkg/garden/lib/mip.hoon
Normal file
55
pkg/garden/lib/mip.hoon
Normal file
@ -0,0 +1,55 @@
|
||||
|%
|
||||
++ mip :: map of maps
|
||||
|$ [kex key value]
|
||||
(map kex (map key value))
|
||||
::
|
||||
++ bi :: mip engine
|
||||
=| a=(map * (map))
|
||||
|@
|
||||
++ del
|
||||
|* [b=* c=*]
|
||||
=+ d=(~(gut by a) b ~)
|
||||
=+ e=(~(del by d) c)
|
||||
?~ e
|
||||
(~(del by a) b)
|
||||
(~(put by a) b e)
|
||||
::
|
||||
++ get
|
||||
|* [b=* c=*]
|
||||
=> .(b `_?>(?=(^ a) p.n.a)`b, c `_?>(?=(^ a) ?>(?=(^ q.n.a) p.n.q.n.a))`c)
|
||||
^- (unit _?>(?=(^ a) ?>(?=(^ q.n.a) q.n.q.n.a)))
|
||||
(~(get by (~(gut by a) b ~)) c)
|
||||
::
|
||||
++ got
|
||||
|* [b=* c=*]
|
||||
(need (get b c))
|
||||
::
|
||||
++ gut
|
||||
|* [b=* c=* d=*]
|
||||
(~(gut by (~(gut by a) b ~)) c d)
|
||||
::
|
||||
++ has
|
||||
|* [b=* c=*]
|
||||
!=(~ (get b c))
|
||||
::
|
||||
++ key
|
||||
|* b=*
|
||||
~(key by (~(gut by a) b ~))
|
||||
::
|
||||
++ put
|
||||
|* [b=* c=* d=*]
|
||||
%+ ~(put by a) b
|
||||
%. [c d]
|
||||
%~ put by
|
||||
(~(gut by a) b ~)
|
||||
::
|
||||
++ tap
|
||||
::NOTE naive turn-based implementation find-errors ):
|
||||
=< $
|
||||
=+ b=`_?>(?=(^ a) *(list [x=_p.n.a _?>(?=(^ q.n.a) [y=p v=q]:n.q.n.a)]))`~
|
||||
|. ^+ b
|
||||
?~ a
|
||||
b
|
||||
$(a r.a, b (welp (turn ~(tap by q.n.a) (lead p.n.a)) $(a l.a)))
|
||||
--
|
||||
--
|
@ -11,11 +11,16 @@
|
||||
%all (settings +.dat)
|
||||
%bucket (bucket +.dat)
|
||||
%entry (value +.dat)
|
||||
%desk (desk-settings +.dat)
|
||||
==
|
||||
::
|
||||
++ settings
|
||||
|= s=^settings
|
||||
^- json
|
||||
[%o (~(run by s) desk-settings)]
|
||||
::
|
||||
++ desk-settings
|
||||
|= s=(map key ^bucket)
|
||||
[%o (~(run by s) bucket)]
|
||||
::
|
||||
++ event
|
||||
@ -30,35 +35,39 @@
|
||||
==
|
||||
::
|
||||
++ put-bucket
|
||||
|= [k=key b=^bucket]
|
||||
|= [d=desk k=key b=^bucket]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ bucket-key+s+k
|
||||
bucket+(bucket b)
|
||||
desk+s+d
|
||||
==
|
||||
::
|
||||
++ del-bucket
|
||||
|= k=key
|
||||
|= [d=desk k=key]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ bucket-key+s+k
|
||||
desk+s+d
|
||||
==
|
||||
::
|
||||
++ put-entry
|
||||
|= [b=key k=key v=val]
|
||||
|= [d=desk b=key k=key v=val]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ bucket-key+s+b
|
||||
entry-key+s+k
|
||||
value+(value v)
|
||||
desk+s+d
|
||||
==
|
||||
::
|
||||
++ del-entry
|
||||
|= [buc=key =key]
|
||||
|= [d=desk buc=key =key]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ bucket-key+s+buc
|
||||
entry-key+s+key
|
||||
desk+s+d
|
||||
==
|
||||
::
|
||||
++ value
|
||||
@ -93,25 +102,29 @@
|
||||
::
|
||||
++ put-bucket
|
||||
%- ot
|
||||
:~ bucket-key+so
|
||||
:~ desk+so
|
||||
bucket-key+so
|
||||
bucket+bucket
|
||||
==
|
||||
::
|
||||
++ del-bucket
|
||||
%- ot
|
||||
:~ bucket-key+so
|
||||
:~ desk+so
|
||||
bucket-key+so
|
||||
==
|
||||
::
|
||||
++ put-entry
|
||||
%- ot
|
||||
:~ bucket-key+so
|
||||
:~ desk+so
|
||||
bucket-key+so
|
||||
entry-key+so
|
||||
value+value
|
||||
==
|
||||
::
|
||||
++ del-entry
|
||||
%- ot
|
||||
:~ bucket-key+so
|
||||
:~ desk+so
|
||||
bucket-key+so
|
||||
entry-key+so
|
||||
==
|
||||
::
|
1
pkg/garden/sur/settings.hoon
Symbolic link
1
pkg/garden/sur/settings.hoon
Symbolic link
@ -0,0 +1 @@
|
||||
../../garden-dev/sur/settings.hoon
|
@ -29,6 +29,7 @@ module.exports = {
|
||||
'no-undef': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
'no-unused-expressions': ['error', { allowShortCircuit: true }],
|
||||
'no-use-before-define': 'off',
|
||||
'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['draft'] }],
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
|
6961
pkg/grid/package-lock.json
generated
6961
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@
|
||||
"@radix-ui/react-polymorphic": "^0.0.13",
|
||||
"@radix-ui/react-portal": "^0.0.15",
|
||||
"@radix-ui/react-toggle": "^0.0.10",
|
||||
"@tlon/sigil-js": "^1.4.4",
|
||||
"@urbit/api": "^1.4.0",
|
||||
"@urbit/http-api": "^1.3.1",
|
||||
"classnames": "^2.3.1",
|
||||
@ -26,24 +27,24 @@
|
||||
"color2k": "^1.2.4",
|
||||
"fuzzy": "^0.1.3",
|
||||
"immer": "^9.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"postcss-import": "^14.0.2",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"slugify": "^1.6.0",
|
||||
"zustand": "^3.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/aspect-ratio": "^0.2.1",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@types/mousetrap": "^1.6.8",
|
||||
"@types/node": "^16.7.9",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react": "^16.0.0",
|
||||
"@types/react-dom": "^16.0.0",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
@ -66,6 +67,7 @@
|
||||
"rollup-plugin-analyzer": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^5.5.2",
|
||||
"tailwindcss": "^2.2.7",
|
||||
"tailwindcss-theming": "^3.0.0-beta.3",
|
||||
"tailwindcss-touch": "^1.0.1",
|
||||
"typescript": "^4.3.2",
|
||||
"vite": "^2.4.4",
|
||||
|
@ -1,13 +1,40 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { BrowserRouter, Switch, Route, useHistory } from 'react-router-dom';
|
||||
import { Grid } from './pages/Grid';
|
||||
import useDocketState from './state/docket';
|
||||
import {PermalinkRoutes} from './pages/PermalinkRoutes';
|
||||
import { PermalinkRoutes } from './pages/PermalinkRoutes';
|
||||
import useKilnState from './state/kiln';
|
||||
import { usePreferencesStore } from './nav/preferences/usePreferencesStore';
|
||||
import useContactState from './state/contact';
|
||||
import api from './state/api';
|
||||
|
||||
const AppRoutes = () => {
|
||||
const { push } = useHistory();
|
||||
const theme = usePreferencesStore((s) => s.theme);
|
||||
|
||||
const updateThemeClass = useCallback(
|
||||
(e: MediaQueryListEvent) => {
|
||||
if ((e.matches && theme === 'automatic') || theme === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
usePreferencesStore.setState({ currentTheme: 'dark' });
|
||||
} else {
|
||||
document.body.classList.remove('dark');
|
||||
usePreferencesStore.setState({ currentTheme: 'light' });
|
||||
}
|
||||
},
|
||||
[theme]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const query = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
query.addEventListener('change', updateThemeClass);
|
||||
updateThemeClass({ matches: query.matches } as MediaQueryListEvent);
|
||||
return () => {
|
||||
query.removeEventListener('change', updateThemeClass);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.name = 'grid';
|
||||
@ -18,6 +45,8 @@ const AppRoutes = () => {
|
||||
const { fetchVats, fetchLag } = useKilnState.getState();
|
||||
fetchVats();
|
||||
fetchLag();
|
||||
useContactState.getState().initialize(api);
|
||||
|
||||
Mousetrap.bind(['command+/', 'ctrl+/'], () => {
|
||||
push('/leap/search');
|
||||
});
|
||||
|
@ -121,13 +121,13 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
|
||||
</DocketHeader>
|
||||
{vat ? (
|
||||
<>
|
||||
<hr className="-mx-5 sm:-mx-8" />
|
||||
<hr className="-mx-5 sm:-mx-8 border-gray-50" />
|
||||
<VatMeta vat={vat} />
|
||||
</>
|
||||
) : null}
|
||||
{'chad' in docket ? null : (
|
||||
<>
|
||||
<hr className="-mx-5 sm:-mx-8" />
|
||||
<hr className="-mx-5 sm:-mx-8 border-gray-50" />
|
||||
<TreatyMeta treaty={docket} />
|
||||
</>
|
||||
)}
|
||||
|
@ -3,6 +3,7 @@ import React, { HTMLProps, ReactNode } from 'react';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { DocketWithDesk } from '../state/docket';
|
||||
import { getAppHref } from '../state/util';
|
||||
import { DocketImage } from './DocketImage';
|
||||
|
||||
type Sizes = 'xs' | 'small' | 'default';
|
||||
type LinkOrAnchorProps = {
|
||||
@ -19,12 +20,6 @@ export type AppLinkProps<T extends DocketWithDesk> = Omit<LinkOrAnchorProps, 'to
|
||||
to?: (app: T) => LinkProps['to'] | undefined;
|
||||
};
|
||||
|
||||
const sizeMap: Record<Sizes, string> = {
|
||||
xs: 'w-6 h-6 mr-2 rounded',
|
||||
small: 'w-8 h-8 mr-3 rounded-lg',
|
||||
default: 'w-12 h-12 mr-3 rounded-lg'
|
||||
};
|
||||
|
||||
export const AppLink = <T extends DocketWithDesk>({
|
||||
app,
|
||||
to,
|
||||
@ -35,8 +30,11 @@ export const AppLink = <T extends DocketWithDesk>({
|
||||
}: AppLinkProps<T>) => {
|
||||
const linkTo = to?.(app);
|
||||
const linkClassnames = classNames(
|
||||
'flex items-center default-ring ring-offset-2 rounded-lg',
|
||||
selected && 'ring-4',
|
||||
'flex items-center default-ring rounded-lg',
|
||||
size === 'default' && 'ring-offset-2',
|
||||
size !== 'xs' && 'p-2',
|
||||
size === 'xs' && 'p-1',
|
||||
selected && 'bg-blue-200',
|
||||
className
|
||||
);
|
||||
const link = (children: ReactNode) =>
|
||||
@ -51,18 +49,7 @@ export const AppLink = <T extends DocketWithDesk>({
|
||||
);
|
||||
return link(
|
||||
<>
|
||||
<div
|
||||
className={classNames('flex-none relative bg-gray-200', sizeMap[size])}
|
||||
style={{ backgroundColor: app.color }}
|
||||
>
|
||||
{app.image && (
|
||||
<img
|
||||
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={app.image}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DocketImage color={app.color} image={app.image} size={size} />
|
||||
<div className="flex-1 text-black">
|
||||
<p>{app.title}</p>
|
||||
{app.info && size === 'default' && <p className="font-normal">{app.info}</p>}
|
||||
|
@ -11,7 +11,7 @@ type AppListProps<T extends DocketWithDesk> = {
|
||||
matchAgainst?: MatchItem;
|
||||
onClick?: (e: MouseEvent<HTMLAnchorElement>, app: T) => void;
|
||||
listClass?: string;
|
||||
} & Omit<AppLinkProps<T>, 'app' | 'onClick'>;
|
||||
} & Omit<AppLinkProps, 'app' | 'onClick'>;
|
||||
|
||||
export function appMatches(target: DocketWithDesk, match?: MatchItem): boolean {
|
||||
if (!match) {
|
||||
@ -27,7 +27,7 @@ export const AppList = <T extends DocketWithDesk>({
|
||||
labelledBy,
|
||||
matchAgainst,
|
||||
onClick,
|
||||
listClass = 'space-y-8',
|
||||
listClass,
|
||||
size = 'default',
|
||||
...props
|
||||
}: AppListProps<T>) => {
|
||||
@ -37,9 +37,9 @@ export const AppList = <T extends DocketWithDesk>({
|
||||
return (
|
||||
<ul
|
||||
className={classNames(
|
||||
size === 'default' && 'space-y-8',
|
||||
size === 'small' && 'space-y-4',
|
||||
size === 'xs' && 'space-y-2',
|
||||
size === 'default' && 'space-y-4',
|
||||
size !== 'xs' && '-mx-2',
|
||||
size === 'xs' && '-mx-1',
|
||||
listClass
|
||||
)}
|
||||
aria-labelledby={labelledBy}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { capitalize } from 'lodash-es';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
interface AttributeProps {
|
||||
attr: string;
|
||||
|
85
pkg/grid/src/components/Avatar.tsx
Normal file
85
pkg/grid/src/components/Avatar.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo } from 'react';
|
||||
import { sigil, reactRenderer } from '@tlon/sigil-js';
|
||||
import { deSig, Contact } from '@urbit/api';
|
||||
|
||||
export type AvatarSizes = 'xs' | 'small' | 'default';
|
||||
|
||||
interface AvatarProps extends Contact {
|
||||
shipName: string;
|
||||
size: AvatarSizes;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface AvatarMeta {
|
||||
classes: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
const sizeMap: Record<AvatarSizes, AvatarMeta> = {
|
||||
xs: { classes: 'w-6 h-6 rounded', size: 12 },
|
||||
small: { classes: 'w-8 h-8 rounded-lg', size: 16 },
|
||||
default: { classes: 'w-12 h-12 rounded-lg', size: 24 }
|
||||
};
|
||||
|
||||
const foregroundFromBackground = (background: string): 'black' | 'white' => {
|
||||
const rgb = {
|
||||
r: parseInt(background.slice(1, 3), 16),
|
||||
g: parseInt(background.slice(3, 5), 16),
|
||||
b: parseInt(background.slice(5, 7), 16)
|
||||
};
|
||||
const brightness = (299 * rgb.r + 587 * rgb.g + 114 * rgb.b) / 1000;
|
||||
const whiteBrightness = 255;
|
||||
|
||||
return whiteBrightness - brightness < 50 ? 'black' : 'white';
|
||||
};
|
||||
|
||||
const emptyContact: Contact = {
|
||||
nickname: '',
|
||||
bio: '',
|
||||
status: '',
|
||||
color: '#000000',
|
||||
avatar: null,
|
||||
cover: null,
|
||||
groups: [],
|
||||
'last-updated': 0
|
||||
};
|
||||
|
||||
export const Avatar = ({ size, className, ...ship }: AvatarProps) => {
|
||||
const { shipName, color, avatar } = { ...emptyContact, ...ship };
|
||||
const { classes, size: sigilSize } = sizeMap[size];
|
||||
const foregroundColor = foregroundFromBackground(color);
|
||||
const sigilElement = useMemo(() => {
|
||||
if (shipName.match(/[_^]/)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sigil({
|
||||
patp: deSig(shipName) || 'zod',
|
||||
renderer: reactRenderer,
|
||||
size: sigilSize,
|
||||
icon: true,
|
||||
colors: [color, foregroundColor]
|
||||
});
|
||||
}, [shipName, color, foregroundColor]);
|
||||
|
||||
if (avatar) {
|
||||
return <img className={classNames('', classes)} src={avatar} alt="" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex-none relative bg-black rounded-lg',
|
||||
classes,
|
||||
size === 'xs' && 'p-1.5',
|
||||
size === 'small' && 'p-2',
|
||||
size === 'default' && 'p-3',
|
||||
className
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{sigilElement}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -20,10 +20,10 @@ type PolymorphicButton = Polymorphic.ForwardRefComponent<
|
||||
const variants: Record<ButtonVariant, string> = {
|
||||
primary: 'text-white bg-black',
|
||||
secondary: 'text-black bg-gray-100',
|
||||
caution: 'text-white bg-orange-500',
|
||||
destructive: 'text-white bg-red-400',
|
||||
caution: 'text-white bg-orange-400',
|
||||
destructive: 'text-white bg-red-500',
|
||||
'alt-primary': 'text-white bg-blue-400',
|
||||
'alt-secondary': 'text-blue-400 bg-blue-100'
|
||||
'alt-secondary': 'text-blue-400 bg-blue-50'
|
||||
};
|
||||
|
||||
export const Button = React.forwardRef(
|
||||
|
@ -1,29 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Docket } from '@urbit/api/docket';
|
||||
import cn from 'classnames';
|
||||
import { useTileColor } from '../tiles/useTileColor';
|
||||
|
||||
type DocketImageSizes = 'xs' | 'small' | 'default' | 'full';
|
||||
|
||||
interface DocketImageProps extends Pick<Docket, 'color' | 'image'> {
|
||||
className?: string;
|
||||
sizing?: 'small' | 'full';
|
||||
size?: DocketImageSizes;
|
||||
}
|
||||
|
||||
export function DocketImage({ color, image, className = '', sizing = 'full' }: DocketImageProps) {
|
||||
const sizingClass =
|
||||
sizing === 'full'
|
||||
? 'w-full h-full md:w-full md:h-full rounded-md'
|
||||
: 'w-12 h-12 md:w-20 md:h-20 rounded-xl';
|
||||
const sizeMap: Record<DocketImageSizes, string> = {
|
||||
xs: 'w-6 h-6 mr-2 rounded',
|
||||
small: 'w-8 h-8 mr-3 rounded-md',
|
||||
default: 'w-12 h-12 mr-3 rounded-lg',
|
||||
full: 'w-20 h-20 md:w-32 md:h-32 rounded-2xl'
|
||||
};
|
||||
|
||||
export function DocketImage({ color, image, className = '', size = 'full' }: DocketImageProps) {
|
||||
const { tileColor } = useTileColor(color);
|
||||
return (
|
||||
<div
|
||||
className={cn(sizingClass, `flex-none relative bg-gray-200`, className)}
|
||||
style={{ backgroundColor: color }}
|
||||
className={cn('flex-none relative bg-gray-200 overflow-hidden', sizeMap[size], className)}
|
||||
style={{ backgroundColor: tileColor }}
|
||||
>
|
||||
{image && (
|
||||
<img
|
||||
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={image}
|
||||
alt=""
|
||||
/>
|
||||
<img className="absolute top-0 left-0 h-full w-full object-contain" src={image} alt="" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,12 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { Provider } from '@urbit/api';
|
||||
import { Contact, Provider } from '@urbit/api';
|
||||
import { ShipName } from './ShipName';
|
||||
import { Avatar, AvatarSizes } from './Avatar';
|
||||
|
||||
export type ProviderLinkProps = Omit<LinkProps, 'to'> & {
|
||||
provider: Provider;
|
||||
small?: boolean;
|
||||
provider: { shipName: string } & Contact;
|
||||
size?: AvatarSizes;
|
||||
selected?: boolean;
|
||||
to?: (p: Provider) => LinkProps['to'];
|
||||
};
|
||||
@ -15,31 +16,26 @@ export const ProviderLink = ({
|
||||
provider,
|
||||
to,
|
||||
selected = false,
|
||||
small = false,
|
||||
size = 'default',
|
||||
className,
|
||||
...props
|
||||
}: ProviderLinkProps) => {
|
||||
const small = size === 'small' || size === 'xs';
|
||||
return (
|
||||
<Link
|
||||
to={(to && to(provider)) || `/leap/search/${provider.shipName}/apps`}
|
||||
className={classNames(
|
||||
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
|
||||
selected && 'ring-4',
|
||||
'flex items-center p-2 space-x-3 default-ring rounded-lg',
|
||||
!small && 'ring-offset-2',
|
||||
selected && 'bg-blue-200',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex-none relative bg-black rounded-lg',
|
||||
small ? 'w-8 h-8' : 'w-12 h-12'
|
||||
)}
|
||||
>
|
||||
{/* TODO: Handle sigils */}
|
||||
</div>
|
||||
<Avatar size={size} {...provider} />
|
||||
<div className="flex-1 text-black">
|
||||
<p className="font-mono">{provider.nickname || <ShipName name={provider.shipName} />}</p>
|
||||
{provider.status && !small && <p className="font-normal">{provider.status}</p>}
|
||||
{provider.status && size === 'default' && <p className="font-normal">{provider.status}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { MouseEvent, useCallback } from 'react';
|
||||
import { Provider } from '@urbit/api';
|
||||
import { Contact, Provider } from '@urbit/api';
|
||||
import classNames from 'classnames';
|
||||
import { MatchItem } from '../nav/Nav';
|
||||
import { useRecentsStore } from '../nav/search/Home';
|
||||
import { ProviderLink, ProviderLinkProps } from './ProviderLink';
|
||||
|
||||
export type ProviderListProps = {
|
||||
providers: Provider[];
|
||||
providers: ({ shipName: string } & Contact)[];
|
||||
labelledBy: string;
|
||||
matchAgainst?: MatchItem;
|
||||
onClick?: (e: MouseEvent<HTMLAnchorElement>, p: Provider) => void;
|
||||
@ -28,7 +28,7 @@ export const ProviderList = ({
|
||||
matchAgainst,
|
||||
onClick,
|
||||
listClass,
|
||||
small = false,
|
||||
size = 'default',
|
||||
...props
|
||||
}: ProviderListProps) => {
|
||||
const addRecentDev = useRecentsStore((state) => state.addRecentDev);
|
||||
@ -39,18 +39,18 @@ export const ProviderList = ({
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={classNames(small ? 'space-y-4' : 'space-y-8', listClass)}
|
||||
className={classNames(size !== 'default' ? 'space-y-4' : 'space-y-8', listClass)}
|
||||
aria-labelledby={labelledBy}
|
||||
>
|
||||
{providers.map((p) => (
|
||||
<li key={p.shipName} id={p.shipName} role="option" aria-selected={selected(p)}>
|
||||
<ProviderLink
|
||||
{...props}
|
||||
small={small}
|
||||
size={size}
|
||||
provider={p}
|
||||
selected={selected(p)}
|
||||
onClick={(e) => {
|
||||
addRecentDev(p);
|
||||
addRecentDev(p.shipName);
|
||||
if (onClick) {
|
||||
onClick(e, p);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { debounce, DebounceSettings } from 'lodash-es';
|
||||
import { debounce, DebounceSettings } from 'lodash';
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useIsMounted } from './useIsMounted';
|
||||
|
||||
|
@ -1,14 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLeapStore } from './Nav';
|
||||
import React from 'react';
|
||||
import helpAndSupport from '../assets/help-and-support.svg';
|
||||
|
||||
export const Help = () => {
|
||||
const select = useLeapStore((state) => state.select);
|
||||
|
||||
useEffect(() => {
|
||||
select('Help and Support');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center px-4 py-8 md:px-8 md:py-16 space-y-8 md:space-y-16">
|
||||
<img className="w-52 h-auto" src={helpAndSupport} alt="" />
|
||||
|
@ -34,10 +34,10 @@ export function createPreviousPath(current: string): string {
|
||||
type LeapProps = {
|
||||
menu: MenuState;
|
||||
dropdown: string;
|
||||
showClose: boolean;
|
||||
navOpen: boolean;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }: LeapProps, ref) => {
|
||||
export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: LeapProps, ref) => {
|
||||
const { push } = useHistory();
|
||||
const match = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>(
|
||||
`/leap/${menu}/:query?/(apps)?/:desk?`
|
||||
@ -196,47 +196,51 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
className={classNames(
|
||||
'relative z-50 flex items-center w-full px-2 rounded-full bg-white default-ring focus-within:ring-4',
|
||||
className
|
||||
)}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<label
|
||||
htmlFor="leap"
|
||||
<div className="relative z-50 w-full">
|
||||
<form
|
||||
className={classNames(
|
||||
'inline-block flex-none p-2 h4 text-blue-400',
|
||||
!selection && 'sr-only'
|
||||
'flex items-center h-full w-full px-2 rounded-full bg-white default-ring focus-within:ring-2',
|
||||
navOpen && menu !== 'search' && 'opacity-60',
|
||||
!navOpen ? 'bg-gray-50' : '',
|
||||
className
|
||||
)}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{selection || 'Search Landscape'}
|
||||
</label>
|
||||
<input
|
||||
id="leap"
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
placeholder={selection ? '' : 'Search Landscape'}
|
||||
className="flex-1 w-full h-full px-2 h4 rounded-full bg-transparent outline-none"
|
||||
value={rawInput}
|
||||
onClick={toggleSearch}
|
||||
onFocus={onFocus}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
aria-autocomplete="both"
|
||||
aria-controls={dropdown}
|
||||
aria-activedescendant={selectedMatch?.display || selectedMatch?.value}
|
||||
/>
|
||||
{showClose && (
|
||||
<label
|
||||
htmlFor="leap"
|
||||
className={classNames(
|
||||
'inline-block flex-none p-2 h4 text-blue-400',
|
||||
!selection && 'sr-only'
|
||||
)}
|
||||
>
|
||||
{selection || 'Search Landscape'}
|
||||
</label>
|
||||
<input
|
||||
id="leap"
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
placeholder={selection ? '' : 'Search Landscape'}
|
||||
className="flex-1 w-full h-full px-2 h4 rounded-full bg-transparent outline-none"
|
||||
value={rawInput}
|
||||
onClick={toggleSearch}
|
||||
onFocus={onFocus}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
aria-autocomplete="both"
|
||||
aria-controls={dropdown}
|
||||
aria-activedescendant={selectedMatch?.display || selectedMatch?.value}
|
||||
/>
|
||||
</form>
|
||||
{navOpen && (
|
||||
<Link
|
||||
to="/"
|
||||
className="circle-button w-8 h-8 text-gray-400 bg-gray-100 default-ring"
|
||||
className="absolute top-1/2 right-2 flex-none circle-button w-8 h-8 text-gray-400 bg-gray-50 default-ring -translate-y-1/2"
|
||||
onClick={() => select(null)}
|
||||
>
|
||||
<Cross className="w-3 h-3 fill-current" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Link>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { DialogContent } from '@radix-ui/react-dialog';
|
||||
import * as Portal from '@radix-ui/react-portal';
|
||||
import classNames from 'classnames';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import { Route, Switch, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import create from 'zustand';
|
||||
import { Dialog } from '../components/Dialog';
|
||||
import { Help } from './Help';
|
||||
@ -88,7 +88,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const navRef = useRef<HTMLDivElement>(null);
|
||||
const dialogNavRef = useRef<HTMLDivElement>(null);
|
||||
const [systemMenuOpen, setSystemMenuOpen] = useState(false);
|
||||
const systemMenuOpen = useRouteMatch('/system-menu');
|
||||
const [dialogContentOpen, setDialogContentOpen] = useState(false);
|
||||
const select = useLeapStore((state) => state.select);
|
||||
|
||||
@ -123,15 +123,6 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disableCloseWhenDropdownOpen = useCallback(
|
||||
(e: Event) => {
|
||||
if (systemMenuOpen) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
[systemMenuOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Using portal so that we can retain the same nav items both in the dialog and in the base header */}
|
||||
@ -140,24 +131,18 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
className="flex justify-center w-full space-x-2"
|
||||
>
|
||||
<SystemMenu
|
||||
open={systemMenuOpen}
|
||||
setOpen={setSystemMenuOpen}
|
||||
showOverlay={!isOpen}
|
||||
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-100')}
|
||||
/>
|
||||
<NotificationsLink isOpen={isOpen} />
|
||||
<Leap
|
||||
ref={inputRef}
|
||||
open={!!systemMenuOpen}
|
||||
menu={menuState}
|
||||
dropdown="leap-items"
|
||||
showClose={isOpen}
|
||||
className={classNames('flex-1 max-w-[600px]', !isOpen ? 'bg-gray-100' : '')}
|
||||
navOpen={isOpen}
|
||||
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-50')}
|
||||
/>
|
||||
<NotificationsLink menu={menuState} navOpen={isOpen} />
|
||||
<Leap ref={inputRef} menu={menuState} dropdown="leap-items" navOpen={isOpen} />
|
||||
</Portal.Root>
|
||||
<div
|
||||
ref={navRef}
|
||||
className={classNames(
|
||||
'w-full max-w-3xl my-6 px-4 text-gray-400 font-semibold',
|
||||
'w-full max-w-[712px] mx-auto my-6 text-gray-400 font-semibold',
|
||||
dialogContentOpen && 'h-12'
|
||||
)}
|
||||
role="combobox"
|
||||
@ -168,17 +153,19 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
<Dialog open={isOpen} onOpenChange={onDialogClose}>
|
||||
<DialogContent
|
||||
onOpenAutoFocus={onOpen}
|
||||
onInteractOutside={disableCloseWhenDropdownOpen}
|
||||
className="fixed bottom-0 sm:top-0 scroll-left-50 flex flex-col scroll-full-width max-w-3xl px-4 pb-4 text-gray-400 -translate-x-1/2 outline-none"
|
||||
className="fixed bottom-0 sm:top-0 sm:bottom-auto scroll-left-50 flex flex-col scroll-full-width max-w-[882px] px-4 sm:pb-4 text-gray-400 -translate-x-1/2 outline-none"
|
||||
role="combobox"
|
||||
aria-controls="leap-items"
|
||||
aria-owns="leap-items"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<header ref={dialogNavRef} className="my-6 order-last sm:order-none" />
|
||||
<header
|
||||
ref={dialogNavRef}
|
||||
className="max-w-[712px] w-full mx-auto mt-6 mb-3 order-last sm:order-none"
|
||||
/>
|
||||
<div
|
||||
id="leap-items"
|
||||
className="grid grid-rows-[fit-content(100vh)] bg-white rounded-3xl overflow-hidden default-ring"
|
||||
className="grid grid-rows-[fit-content(calc(100vh-6.25rem))] bg-white rounded-3xl overflow-hidden default-ring focus-visible:ring-2"
|
||||
tabIndex={0}
|
||||
role="listbox"
|
||||
>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLeapStore } from './Nav';
|
||||
import { Button } from '../components/Button';
|
||||
import { Notification } from '../state/hark-types';
|
||||
import { BasicNotification } from './notifications/BasicNotification';
|
||||
@ -27,12 +26,12 @@ const Empty = () => (
|
||||
);
|
||||
|
||||
export const Notifications = () => {
|
||||
const select = useLeapStore((s) => s.select);
|
||||
// const select = useLeapStore((s) => s.select);
|
||||
const { notifications, systemNotifications, hasAnyNotifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
select('Notifications');
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// select('Notifications');
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[auto,1fr] h-full p-4 md:p-8 overflow-hidden">
|
||||
|
@ -4,6 +4,7 @@ import { Link, LinkProps } from 'react-router-dom';
|
||||
import { Bullet } from '../components/icons/Bullet';
|
||||
import { Notification } from '../state/hark-types';
|
||||
import { useNotifications } from '../state/notifications';
|
||||
import { MenuState } from './Nav';
|
||||
|
||||
type NotificationsState = 'empty' | 'unread' | 'attention-needed';
|
||||
|
||||
@ -24,10 +25,11 @@ function getNotificationsState(
|
||||
}
|
||||
|
||||
type NotificationsLinkProps = Omit<LinkProps<HTMLAnchorElement>, 'to'> & {
|
||||
isOpen: boolean;
|
||||
menu: MenuState;
|
||||
navOpen: boolean;
|
||||
};
|
||||
|
||||
export const NotificationsLink = ({ isOpen }: NotificationsLinkProps) => {
|
||||
export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) => {
|
||||
const { notifications, systemNotifications } = useNotifications();
|
||||
const state = getNotificationsState(notifications, systemNotifications);
|
||||
|
||||
@ -35,12 +37,13 @@ export const NotificationsLink = ({ isOpen }: NotificationsLinkProps) => {
|
||||
<Link
|
||||
to="/leap/notifications"
|
||||
className={classNames(
|
||||
'relative z-50 flex-none circle-button h4',
|
||||
isOpen && 'text-opacity-60',
|
||||
state === 'empty' && !isOpen && 'text-gray-400 bg-gray-100',
|
||||
state === 'empty' && isOpen && 'text-gray-400 bg-white',
|
||||
'relative z-50 flex-none circle-button h4 default-ring',
|
||||
navOpen && 'text-opacity-60',
|
||||
navOpen && menu !== 'notifications' && 'opacity-60',
|
||||
state === 'empty' && !navOpen && 'text-gray-400 bg-gray-50',
|
||||
state === 'empty' && navOpen && 'text-gray-400 bg-white',
|
||||
state === 'unread' && 'bg-blue-400 text-white',
|
||||
state === 'attention-needed' && 'text-white bg-orange-500'
|
||||
state === 'attention-needed' && 'text-white bg-orange-400'
|
||||
)}
|
||||
>
|
||||
{state === 'empty' && <Bullet className="w-6 h-6" />}
|
||||
|
@ -2,18 +2,29 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import clipboardCopy from 'clipboard-copy';
|
||||
import React, { HTMLAttributes, useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, Route, useHistory } from 'react-router-dom';
|
||||
import { Vat } from '@urbit/api/hood';
|
||||
import { Adjust } from '../components/icons/Adjust';
|
||||
import { disableDefault } from '../state/util';
|
||||
import { useVat } from '../state/kiln';
|
||||
import { disableDefault, handleDropdownLink } from '../state/util';
|
||||
import { MenuState } from './Nav';
|
||||
|
||||
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
menu: MenuState;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
showOverlay?: boolean;
|
||||
navOpen: boolean;
|
||||
};
|
||||
|
||||
export const SystemMenu = ({ open, setOpen, className, showOverlay = false }: SystemMenuProps) => {
|
||||
function getHash(vat: Vat): string {
|
||||
const parts = vat.hash.split('.');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
export const SystemMenu = ({ className, menu, open, navOpen }: SystemMenuProps) => {
|
||||
const { push } = useHistory();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const garden = useVat('garden');
|
||||
const hash = garden ? getHash(garden) : null;
|
||||
|
||||
const copyHash = useCallback((event: Event) => {
|
||||
event.preventDefault();
|
||||
@ -26,65 +37,100 @@ export const SystemMenu = ({ open, setOpen, className, showOverlay = false }: Sy
|
||||
}, 1250);
|
||||
}, []);
|
||||
|
||||
const preventFlash = useCallback((e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
if (target.id !== 'system-menu-overlay') {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Root open={open} onOpenChange={(isOpen) => setOpen(isOpen)}>
|
||||
<DropdownMenu.Trigger
|
||||
className={classNames('circle-button default-ring', open && 'text-gray-300', className)}
|
||||
<div className="z-40">
|
||||
<DropdownMenu.Root
|
||||
modal={false}
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => setTimeout(() => !isOpen && push('/'), 15)}
|
||||
>
|
||||
<Adjust className="w-6 h-6 fill-current" />
|
||||
<span className="sr-only">System Menu</span>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
onCloseAutoFocus={disableDefault}
|
||||
sideOffset={12}
|
||||
className="dropdown min-w-64 p-6 font-semibold text-gray-500 bg-white"
|
||||
>
|
||||
<DropdownMenu.Group className="space-y-6">
|
||||
<DropdownMenu.Item
|
||||
as={Link}
|
||||
to="/leap/system-preferences"
|
||||
className="flex items-center space-x-2 default-ring ring-offset-2 rounded"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen(false), 0);
|
||||
}}
|
||||
<DropdownMenu.Trigger
|
||||
as={Link}
|
||||
to="/system-menu"
|
||||
className={classNames(
|
||||
'appearance-none circle-button default-ring',
|
||||
open && 'text-gray-300',
|
||||
navOpen &&
|
||||
menu !== 'system-preferences' &&
|
||||
menu !== 'help-and-support' &&
|
||||
'opacity-60',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Adjust className="w-6 h-6 fill-current text-gray" />
|
||||
<span className="sr-only">System Menu</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<Route path="/system-menu">
|
||||
<DropdownMenu.Content
|
||||
portalled={false}
|
||||
onCloseAutoFocus={disableDefault}
|
||||
onInteractOutside={preventFlash}
|
||||
onFocusOutside={preventFlash}
|
||||
onPointerDownOutside={preventFlash}
|
||||
sideOffset={12}
|
||||
className="dropdown relative z-40 min-w-64 p-4 font-semibold text-gray-500 bg-white"
|
||||
>
|
||||
<span className="w-5 h-5 bg-gray-100 rounded-full" />
|
||||
<span className="h4">System Preferences</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
as={Link}
|
||||
to="/leap/help-and-support"
|
||||
className="flex items-center space-x-2 default-ring ring-offset-2 rounded"
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen(false), 0);
|
||||
}}
|
||||
>
|
||||
<span className="w-5 h-5 bg-gray-100 rounded-full" />
|
||||
<span className="h4">Help and Support</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
as="button"
|
||||
className="inline-flex items-center py-2 px-3 h4 text-black bg-gray-100 rounded default-ring"
|
||||
onSelect={copyHash}
|
||||
>
|
||||
<span className="sr-only">Base Hash</span>
|
||||
<code>
|
||||
{!copied && <span aria-label="f-j-u-h-l">fjuhl</span>}
|
||||
{copied && 'copied!'}
|
||||
</code>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
{showOverlay && open && (
|
||||
<div className="fixed z-30 right-0 bottom-0 w-screen h-screen bg-black opacity-30" />
|
||||
)}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item
|
||||
as={Link}
|
||||
to="/leap/system-preferences"
|
||||
className="flex items-center p-2 mb-2 space-x-2 focus:bg-blue-200 focus:outline-none rounded"
|
||||
onSelect={handleDropdownLink()}
|
||||
>
|
||||
<span className="w-5 h-5 bg-gray-100 rounded-full" />
|
||||
<span className="h4">System Preferences</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
as={Link}
|
||||
to="/leap/help-and-support"
|
||||
className="flex items-center p-2 mb-2 space-x-2 focus:bg-blue-200 focus:outline-none rounded"
|
||||
onSelect={handleDropdownLink()}
|
||||
>
|
||||
<span className="w-5 h-5 bg-gray-100 rounded-full" />
|
||||
<span className="h4">Help and Support</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
as={Link}
|
||||
to="/app/garden"
|
||||
className="flex items-center p-2 mb-2 space-x-2 focus:bg-blue-200 focus:outline-none rounded"
|
||||
onSelect={handleDropdownLink()}
|
||||
>
|
||||
<span className="w-5 h-5 bg-gray-100 rounded-full" />
|
||||
<span className="h4">About Landscape</span>
|
||||
</DropdownMenu.Item>
|
||||
{hash && (
|
||||
<DropdownMenu.Item
|
||||
as="button"
|
||||
className="inline-flex items-center py-2 px-3 m-2 h4 text-black bg-gray-100 rounded focus:bg-blue-200 focus:outline-none"
|
||||
onSelect={copyHash}
|
||||
>
|
||||
<span className="sr-only">Base Hash</span>
|
||||
<code>
|
||||
{!copied && <span aria-label={hash.split('').join('-')}>{hash}</span>}
|
||||
{copied && 'copied!'}
|
||||
</code>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</Route>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
<Route path="/system-menu">
|
||||
<div
|
||||
id="system-menu-overlay"
|
||||
className="fixed z-30 right-0 bottom-0 w-screen h-screen bg-black opacity-30"
|
||||
/>
|
||||
</Route>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,12 +1,11 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback } 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';
|
||||
import {InterfacePrefs} from './preferences/InterfacePrefs';
|
||||
import { InterfacePrefs } from './preferences/InterfacePrefs';
|
||||
|
||||
interface SystemPreferencesSectionProps extends RouteComponentProps<{ submenu: string }> {
|
||||
submenu: string;
|
||||
@ -22,8 +21,6 @@ function SystemPreferencesSection({
|
||||
icon,
|
||||
text
|
||||
}: SystemPreferencesSectionProps) {
|
||||
const subMatch = useRouteMatch<{ submenu: string }>(`${match.url}/:submenu`);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
@ -42,13 +39,8 @@ function SystemPreferencesSection({
|
||||
|
||||
export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }>) => {
|
||||
const { match } = props;
|
||||
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') {
|
||||
@ -62,11 +54,11 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<aside className="flex-none min-w-60 border-r-2 border-gray-50">
|
||||
<div className="p-8">
|
||||
<input className="input h4 default-ring bg-gray-50" placeholder="Search Preferences" />
|
||||
</div>
|
||||
<nav className="border-b-2 border-gray-100">
|
||||
<nav className="border-b-2 border-gray-50">
|
||||
<ul className="font-semibold">
|
||||
<SystemPreferencesSection
|
||||
{...props}
|
||||
@ -92,7 +84,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
<section className="flex-1 px-5 py-7 text-black">
|
||||
<section className="flex-1 p-8 text-black">
|
||||
<Switch>
|
||||
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
|
||||
<Route path={`${match.url}/interface`} component={InterfacePrefs} />
|
||||
|
@ -14,7 +14,7 @@ type PolymorphicButton = Polymorphic.ForwardRefComponent<
|
||||
const variants: Record<NotificationButtonVariant, string> = {
|
||||
primary: 'text-blue bg-white',
|
||||
secondary: 'text-black bg-white',
|
||||
destructive: 'text-red-400 bg-white'
|
||||
destructive: 'text-red-500 bg-white'
|
||||
};
|
||||
|
||||
export const NotificationButton = React.forwardRef(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { pick } from 'lodash-es';
|
||||
import { pick } from 'lodash';
|
||||
import React, { useCallback } from 'react';
|
||||
import { kilnBump } from '@urbit/api/hood';
|
||||
import { AppList } from '../../components/AppList';
|
||||
@ -23,7 +23,7 @@ export const RuntimeLagNotification = () => (
|
||||
>
|
||||
<header id="system-updates-blocked" className="relative -left-8 space-y-2">
|
||||
<div className="flex space-x-2">
|
||||
<span className="inline-block w-6 h-6 bg-orange-500 rounded-full" />
|
||||
<span className="inline-block w-6 h-6 bg-orange-400 rounded-full" />
|
||||
<span className="font-medium">Landscape</span>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
@ -59,7 +59,7 @@ export const BaseBlockedNotification = ({ notification }: BaseBlockedNotificatio
|
||||
>
|
||||
<header id="system-updates-blocked" className="relative -left-8 space-y-2">
|
||||
<div className="flex space-x-2">
|
||||
<span className="inline-block w-6 h-6 bg-orange-500 rounded-full" />
|
||||
<span className="inline-block w-6 h-6 bg-orange-400 rounded-full" />
|
||||
<span className="font-medium">Landscape</span>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
|
@ -1,21 +1,23 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { Setting } from '../../components/Setting';
|
||||
import { useLeapStore } from '../Nav';
|
||||
import { useSettingsState, SettingsState } from '../../state/settings';
|
||||
import { usePreferencesStore } from './usePreferencesStore';
|
||||
|
||||
export const NotificationPrefs = () => {
|
||||
const select = useLeapStore((s) => s.select);
|
||||
const { doNotDisturb, mentions, toggleDoNotDisturb, toggleMentions } = usePreferencesStore();
|
||||
const selDnd = (s: SettingsState) => s.display.doNotDisturb;
|
||||
async function toggleDnd() {
|
||||
const state = useSettingsState.getState();
|
||||
await state.putEntry('display', 'doNotDisturb', !selDnd(state));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
select('System Preferences: Notifications');
|
||||
}, []);
|
||||
export const NotificationPrefs = () => {
|
||||
const { mentions, toggleMentions } = usePreferencesStore();
|
||||
const doNotDisturb = useSettingsState(selDnd);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="h3 mb-7">Notifications</h2>
|
||||
<div className="space-y-3">
|
||||
<Setting on={doNotDisturb} toggle={toggleDoNotDisturb} name="Do Not Disturb">
|
||||
<Setting on={doNotDisturb} toggle={toggleDnd} name="Do Not Disturb">
|
||||
<p>
|
||||
Block visual desktop notifications whenever Urbit software produces an in-Landscape
|
||||
notification badge.
|
||||
|
@ -4,20 +4,14 @@ 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]);
|
||||
|
@ -4,6 +4,8 @@ import { fakeRequest } from '../../state/util';
|
||||
const useMockData = import.meta.env.MODE === 'mock';
|
||||
|
||||
interface PreferencesStore {
|
||||
theme: 'light' | 'dark' | 'automatic';
|
||||
currentTheme: 'light' | 'dark';
|
||||
otasEnabled: boolean;
|
||||
otaSource: string;
|
||||
doNotDisturb: boolean;
|
||||
@ -15,6 +17,8 @@ interface PreferencesStore {
|
||||
}
|
||||
|
||||
export const usePreferencesStore = create<PreferencesStore>((set) => ({
|
||||
theme: 'automatic',
|
||||
currentTheme: 'light',
|
||||
otasEnabled: true,
|
||||
otaSource: useMockData ? '~sabbus' : '',
|
||||
doNotDisturb: false,
|
||||
|
@ -2,8 +2,7 @@ import produce from 'immer';
|
||||
import create from 'zustand';
|
||||
import React, { useEffect } from 'react';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { take } from 'lodash-es';
|
||||
import { Provider } from '@urbit/api';
|
||||
import { take } from 'lodash';
|
||||
import { MatchItem, useLeapStore } from '../Nav';
|
||||
import { providerMatch } from './Providers';
|
||||
import { AppList } from '../../components/AppList';
|
||||
@ -13,12 +12,13 @@ import { ShipName } from '../../components/ShipName';
|
||||
import { ProviderLink } from '../../components/ProviderLink';
|
||||
import { DocketWithDesk, useCharges } from '../../state/docket';
|
||||
import { getAppHref } from '../../state/util';
|
||||
import useContactState from '../../state/contact';
|
||||
|
||||
export interface RecentsStore {
|
||||
recentApps: DocketWithDesk[];
|
||||
recentDevs: Provider[];
|
||||
recentDevs: string[];
|
||||
addRecentApp: (app: DocketWithDesk) => void;
|
||||
addRecentDev: (dev: Provider) => void;
|
||||
addRecentDev: (ship: string) => void;
|
||||
}
|
||||
|
||||
export const useRecentsStore = create<RecentsStore>(
|
||||
@ -41,7 +41,7 @@ export const useRecentsStore = create<RecentsStore>(
|
||||
addRecentDev: (dev) => {
|
||||
set(
|
||||
produce((draft: RecentsStore) => {
|
||||
const hasDev = draft.recentDevs.find((p) => p.shipName === dev.shipName);
|
||||
const hasDev = draft.recentDevs.includes(dev);
|
||||
if (!hasDev) {
|
||||
draft.recentDevs.unshift(dev);
|
||||
}
|
||||
@ -60,7 +60,7 @@ export const useRecentsStore = create<RecentsStore>(
|
||||
|
||||
window.recents = useRecentsStore.getState;
|
||||
|
||||
export function addRecentDev(dev: Provider) {
|
||||
export function addRecentDev(dev: string) {
|
||||
return useRecentsStore.getState().addRecentDev(dev);
|
||||
}
|
||||
|
||||
@ -73,7 +73,9 @@ export const Home = () => {
|
||||
const { recentApps, recentDevs } = useRecentsStore();
|
||||
const charges = useCharges();
|
||||
const groups = charges?.groups;
|
||||
const zod = { shipName: '~zod' };
|
||||
const contacts = useContactState((s) => s.contacts);
|
||||
const zod = { shipName: '~zod', ...contacts['~zod'] };
|
||||
const providerList = recentDevs.map((d) => ({ shipName: d, ...contacts[d] }));
|
||||
|
||||
useEffect(() => {
|
||||
const apps = recentApps.map((app) => ({
|
||||
@ -91,7 +93,7 @@ export const Home = () => {
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 md:p-8 font-semibold leading-tight text-black overflow-y-auto">
|
||||
<h2 id="recent-apps" className="mb-6 h4 text-gray-500">
|
||||
<h2 id="recent-apps" className="mb-4 h4 text-gray-500">
|
||||
Recent Apps
|
||||
</h2>
|
||||
{recentApps.length === 0 && (
|
||||
@ -115,25 +117,33 @@ export const Home = () => {
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
<hr className="-mx-4 my-6 md:-mx-8 md:my-9" />
|
||||
<h2 id="recent-devs" className="mb-6 h4 text-gray-500">
|
||||
<hr className="-mx-4 my-6 md:-mx-8 md:my-9 border-t-2 border-gray-50" />
|
||||
<h2 id="recent-devs" className="mb-4 h4 text-gray-500">
|
||||
Recent Developers
|
||||
</h2>
|
||||
{recentDevs.length === 0 && (
|
||||
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
|
||||
<p className="mb-4">Urbit app developers you search for will be listed here.</p>
|
||||
<p className="mb-6">
|
||||
Try out app discovery by visiting <ShipName name="~zod" /> below.
|
||||
</p>
|
||||
<ProviderLink provider={zod} small onClick={() => addRecentDev(zod)} />
|
||||
{zod && (
|
||||
<>
|
||||
<p className="mb-6">
|
||||
Try out app discovery by visiting <ShipName name="~zod" /> below.
|
||||
</p>
|
||||
<ProviderLink
|
||||
provider={zod}
|
||||
size="small"
|
||||
onClick={() => addRecentDev(zod.shipName)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{recentDevs.length > 0 && (
|
||||
<ProviderList
|
||||
providers={recentDevs}
|
||||
providers={providerList}
|
||||
labelledBy="recent-devs"
|
||||
matchAgainst={selectedMatch}
|
||||
small
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -5,6 +5,7 @@ import { Provider } from '@urbit/api';
|
||||
import { MatchItem, useLeapStore } from '../Nav';
|
||||
import { useAllies } from '../../state/docket';
|
||||
import { ProviderList } from '../../components/ProviderList';
|
||||
import useContactState from '../../state/contact';
|
||||
|
||||
type ProvidersProps = RouteComponentProps<{ ship: string }>;
|
||||
|
||||
@ -23,6 +24,7 @@ export function providerMatch(provider: Provider | string): MatchItem {
|
||||
export const Providers = ({ match }: ProvidersProps) => {
|
||||
const selectedMatch = useLeapStore((state) => state.selectedMatch);
|
||||
const provider = match?.params.ship;
|
||||
const contacts = useContactState((s) => s.contacts);
|
||||
const allies = useAllies();
|
||||
const search = provider || '';
|
||||
const results = useMemo(
|
||||
@ -39,10 +41,11 @@ export const Providers = ({ match }: ProvidersProps) => {
|
||||
|
||||
return right - left;
|
||||
})
|
||||
.map((el) => ({ shipName: el.original }))
|
||||
.map((el) => ({ shipName: el.original, ...contacts[el.original] }))
|
||||
: [],
|
||||
[allies, search]
|
||||
[allies, search, contacts]
|
||||
);
|
||||
|
||||
const count = results?.length;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { map, omit } from 'lodash-es';
|
||||
import React, { FunctionComponent, useEffect } from 'react';
|
||||
import { map, omit } from 'lodash';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { MenuState, Nav } from '../nav/Nav';
|
||||
import useDocketState, { useCharges } from '../state/docket';
|
||||
import { useKilnState } from '../state/kiln';
|
||||
import { useCharges } from '../state/docket';
|
||||
import { RemoveApp } from '../tiles/RemoveApp';
|
||||
import { SuspendApp } from '../tiles/SuspendApp';
|
||||
import { Tile } from '../tiles/Tile';
|
||||
@ -19,7 +18,7 @@ export const Grid: FunctionComponent<GridProps> = ({ match }) => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<header className="fixed sm:sticky bottom-0 sm:bottom-auto sm:top-0 left-0 z-30 flex justify-center w-full bg-white">
|
||||
<header className="fixed sm:sticky bottom-0 sm:bottom-auto sm:top-0 left-0 z-30 flex justify-center w-full px-4 bg-white">
|
||||
<Nav menu={match.params.menu} />
|
||||
</header>
|
||||
|
||||
|
193
pkg/grid/src/state/base.ts
Normal file
193
pkg/grid/src/state/base.ts
Normal file
@ -0,0 +1,193 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { applyPatches, Patch, produceWithPatches, setAutoFreeze, enablePatches } from 'immer';
|
||||
import { compose } from 'lodash/fp';
|
||||
import _ from 'lodash';
|
||||
import create, { GetState, SetState, UseStore } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import Urbit, { SubscriptionRequestInterface } from '@urbit/http-api';
|
||||
import { Poke } from '@urbit/api';
|
||||
import api from './api';
|
||||
import { useMockData } from './util';
|
||||
|
||||
setAutoFreeze(false);
|
||||
enablePatches();
|
||||
|
||||
export const stateSetter = <T extends Record<string, unknown>>(
|
||||
fn: (state: Readonly<T & BaseState<T>>) => void,
|
||||
set: (newState: T & BaseState<T>) => void,
|
||||
get: () => T & BaseState<T>
|
||||
): void => {
|
||||
const old = get();
|
||||
const [state] = produceWithPatches(old, fn) as readonly [T & BaseState<T>, any, Patch[]];
|
||||
// console.log(patches);
|
||||
set(state);
|
||||
};
|
||||
|
||||
export const optStateSetter = <T extends Record<string, unknown>>(
|
||||
fn: (state: T & BaseState<T>) => void,
|
||||
set: (newState: T & BaseState<T>) => void,
|
||||
get: () => T & BaseState<T>
|
||||
): string => {
|
||||
const old = get();
|
||||
const id = _.uniqueId();
|
||||
const [state, , patches] = produceWithPatches(old, fn) as readonly [
|
||||
T & BaseState<T>,
|
||||
any,
|
||||
Patch[]
|
||||
];
|
||||
set({ ...state, patches: { ...state.patches, [id]: patches } });
|
||||
return id;
|
||||
};
|
||||
|
||||
export const reduceState = <S extends Record<string, unknown>, U>(
|
||||
state: UseStore<S & BaseState<S>>,
|
||||
data: U,
|
||||
reducers: ((data: U, state: S & BaseState<S>) => S & BaseState<S>)[]
|
||||
): void => {
|
||||
const reducer = compose(reducers.map((r) => (sta) => r(data, sta)));
|
||||
state.getState().set((s) => {
|
||||
reducer(s);
|
||||
});
|
||||
};
|
||||
|
||||
export const reduceStateN = <S extends Record<string, unknown>, U>(
|
||||
state: S & BaseState<S>,
|
||||
data: U,
|
||||
reducers: ((data: U, state: S & BaseState<S>) => S & BaseState<S>)[]
|
||||
): void => {
|
||||
const reducer = compose(reducers.map((r) => (sta) => r(data, sta)));
|
||||
state.set(reducer);
|
||||
};
|
||||
|
||||
export const optReduceState = <S extends Record<string, unknown>, U>(
|
||||
state: UseStore<S & BaseState<S>>,
|
||||
data: U,
|
||||
reducers: ((data: U, state: S & BaseState<S>) => BaseState<S> & S)[]
|
||||
): string => {
|
||||
const reducer = compose(reducers.map((r) => (sta) => r(data, sta)));
|
||||
return state.getState().optSet((s) => {
|
||||
reducer(s);
|
||||
});
|
||||
};
|
||||
|
||||
/* eslint-disable-next-line import/no-mutable-exports */
|
||||
export let stateStorageKeys: string[] = [];
|
||||
|
||||
export const stateStorageKey = (stateName: string) => {
|
||||
stateName = `Landscape${stateName}State`;
|
||||
stateStorageKeys = [...new Set([...stateStorageKeys, stateName])];
|
||||
return stateName;
|
||||
};
|
||||
|
||||
(window as any).clearStates = () => {
|
||||
stateStorageKeys.forEach((key) => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
};
|
||||
|
||||
export interface BaseState<StateType extends Record<string, unknown>> {
|
||||
rollback: (id: string) => void;
|
||||
patches: {
|
||||
[id: string]: Patch[];
|
||||
};
|
||||
set: (fn: (state: StateType & BaseState<StateType>) => void) => void;
|
||||
addPatch: (id: string, ...patch: Patch[]) => void;
|
||||
removePatch: (id: string) => void;
|
||||
optSet: (fn: (state: StateType & BaseState<StateType>) => void) => string;
|
||||
initialize: (api: Urbit) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createSubscription(
|
||||
app: string,
|
||||
path: string,
|
||||
e: (data: any) => void
|
||||
): SubscriptionRequestInterface {
|
||||
const request = {
|
||||
app,
|
||||
path,
|
||||
event: e,
|
||||
err: () => {},
|
||||
quit: () => {}
|
||||
};
|
||||
// TODO: err, quit handling (resubscribe?)
|
||||
return request;
|
||||
}
|
||||
|
||||
export const createState = <T extends Record<string, unknown>>(
|
||||
name: string,
|
||||
properties: T | ((set: SetState<T & BaseState<T>>, get: GetState<T & BaseState<T>>) => T),
|
||||
blacklist: (keyof BaseState<T> | keyof T)[] = [],
|
||||
subscriptions: ((
|
||||
set: SetState<T & BaseState<T>>,
|
||||
get: GetState<T & BaseState<T>>
|
||||
) => SubscriptionRequestInterface)[] = []
|
||||
): UseStore<T & BaseState<T>> =>
|
||||
create<T & BaseState<T>>(
|
||||
persist<T & BaseState<T>>(
|
||||
(set, get) => ({
|
||||
initialize: async (airlock: Urbit) => {
|
||||
await Promise.all(subscriptions.map((sub) => airlock.subscribe(sub(set, get))));
|
||||
},
|
||||
set: (fn) => stateSetter(fn, set, get),
|
||||
optSet: (fn) => {
|
||||
return optStateSetter(fn, set, get);
|
||||
},
|
||||
patches: {},
|
||||
addPatch: (id: string, patch: Patch[]) => {
|
||||
set((s) => ({ ...s, patches: { ...s.patches, [id]: patch } }));
|
||||
},
|
||||
removePatch: (id: string) => {
|
||||
set((s) => ({ ...s, patches: _.omit(s.patches, id) }));
|
||||
},
|
||||
rollback: (id: string) => {
|
||||
set((state) => {
|
||||
const applying = state.patches[id];
|
||||
return { ...applyPatches(state, applying), patches: _.omit(state.patches, id) };
|
||||
});
|
||||
},
|
||||
...(typeof properties === 'function' ? (properties as any)(set, get) : properties)
|
||||
}),
|
||||
{
|
||||
blacklist,
|
||||
name: stateStorageKey(name)
|
||||
// version: process.env.LANDSCAPE_SHORTHASH as any
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export async function doOptimistically<A, S extends Record<string, unknown>>(
|
||||
state: UseStore<S & BaseState<S>>,
|
||||
action: A,
|
||||
call: (a: A) => Promise<any>,
|
||||
reduce: ((a: A, fn: S & BaseState<S>) => S & BaseState<S>)[]
|
||||
) {
|
||||
let num: string | undefined;
|
||||
try {
|
||||
num = optReduceState(state, action, reduce);
|
||||
await call(action);
|
||||
state.getState().removePatch(num);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (num) {
|
||||
state.getState().rollback(num);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function pokeOptimisticallyN<A, S extends Record<string, unknown>>(
|
||||
state: UseStore<S & BaseState<S>>,
|
||||
poke: Poke<any>,
|
||||
reduce: ((a: A, fn: S & BaseState<S>) => S & BaseState<S>)[]
|
||||
) {
|
||||
let num: string | undefined;
|
||||
try {
|
||||
num = optReduceState(state, poke.json, reduce);
|
||||
await (useMockData ? new Promise((res) => setTimeout(res, 500)) : api.poke(poke));
|
||||
state.getState().removePatch(num);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (num) {
|
||||
state.getState().rollback(num);
|
||||
}
|
||||
}
|
||||
}
|
134
pkg/grid/src/state/contact.ts
Normal file
134
pkg/grid/src/state/contact.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { Contact, ContactEditFieldPrim, ContactUpdate, deSig, Patp, Rolodex } from '@urbit/api';
|
||||
import { useCallback } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { BaseState, createState, createSubscription, reduceStateN } from './base';
|
||||
import { useMockData } from './util';
|
||||
import { mockContacts } from './mock-data';
|
||||
|
||||
export interface BaseContactState {
|
||||
contacts: Rolodex;
|
||||
isContactPublic: boolean;
|
||||
nackedContacts: Set<Patp>;
|
||||
[ref: string]: unknown;
|
||||
}
|
||||
|
||||
type ContactState = BaseContactState & BaseState<BaseContactState>;
|
||||
|
||||
const initial = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||
const data = _.get(json, 'initial', false);
|
||||
if (data) {
|
||||
state.contacts = data.rolodex;
|
||||
state.isContactPublic = data['is-public'];
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const add = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||
const data = _.get(json, 'add', false);
|
||||
if (data) {
|
||||
state.contacts[data.ship] = data.contact;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const remove = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||
const data = _.get(json, 'remove', false);
|
||||
if (data && data.ship in state.contacts) {
|
||||
delete state.contacts[data.ship];
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const edit = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||
const data = _.get(json, 'edit', false);
|
||||
const ship = `~${deSig(data.ship)}`;
|
||||
if (data && ship in state.contacts) {
|
||||
const [field] = Object.keys(data['edit-field']);
|
||||
if (!field) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const value = data['edit-field'][field];
|
||||
if (field === 'add-group') {
|
||||
if (typeof value !== 'string') {
|
||||
state.contacts[ship].groups.push(`/ship/${Object.values(value).join('/')}`);
|
||||
} else if (!state.contacts[ship].groups.includes(value)) {
|
||||
state.contacts[ship].groups.push(value);
|
||||
}
|
||||
} else if (field === 'remove-group') {
|
||||
if (typeof value !== 'string') {
|
||||
state.contacts[ship].groups = state.contacts[ship].groups.filter(
|
||||
(g) => g !== `/ship/${Object.values(value).join('/')}`
|
||||
);
|
||||
} else {
|
||||
state.contacts[ship].groups = state.contacts[ship].groups.filter((g) => g !== value);
|
||||
}
|
||||
} else {
|
||||
const k = field as ContactEditFieldPrim;
|
||||
state.contacts[ship][k] = value;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const setPublic = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||
const data = _.get(json, 'set-public', state.isContactPublic);
|
||||
state.isContactPublic = data;
|
||||
return state;
|
||||
};
|
||||
|
||||
export const reduceNacks = (
|
||||
json: { resource?: { res: string } },
|
||||
state: ContactState
|
||||
): ContactState => {
|
||||
const data = json?.resource;
|
||||
if (data) {
|
||||
state.nackedContacts.add(`~${data.res}`);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const reduce = [initial, add, remove, edit, setPublic];
|
||||
|
||||
const useContactState = createState<BaseContactState>(
|
||||
'Contact',
|
||||
{
|
||||
contacts: {},
|
||||
nackedContacts: new Set(),
|
||||
isContactPublic: false
|
||||
},
|
||||
['nackedContacts'],
|
||||
[
|
||||
(set, get) =>
|
||||
createSubscription('contact-pull-hook', '/nacks', (e) => {
|
||||
const data = e?.resource;
|
||||
if (data) {
|
||||
reduceStateN(get(), data, [reduceNacks]);
|
||||
}
|
||||
}),
|
||||
(set, get) =>
|
||||
createSubscription('contact-store', '/all', (e) => {
|
||||
const data = _.get(e, 'contact-update', false);
|
||||
if (data) {
|
||||
reduceStateN(get(), data, reduce);
|
||||
}
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
if (useMockData) {
|
||||
useContactState.setState({ contacts: mockContacts });
|
||||
}
|
||||
|
||||
export function useContact(ship: string) {
|
||||
return useContactState(
|
||||
useCallback((s) => s.contacts[`~${deSig(ship)}`] as Contact | null, [ship])
|
||||
);
|
||||
}
|
||||
|
||||
export function useOurContact() {
|
||||
return useContact(`~${window.ship}`);
|
||||
}
|
||||
|
||||
export default useContactState;
|
@ -1,7 +1,7 @@
|
||||
import create from 'zustand';
|
||||
import produce from 'immer';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { omit, pick } from 'lodash-es';
|
||||
import { omit, pick } from 'lodash';
|
||||
import {
|
||||
Allies,
|
||||
Charge,
|
||||
@ -101,7 +101,7 @@ const useDocketState = create<DocketState>((set, get) => ({
|
||||
}
|
||||
if (useMockData) {
|
||||
set((state) => addCharge(state, desk, { ...treaty, chad: { install: null } }));
|
||||
await new Promise<void>((res) => setTimeout(() => res(), 5000));
|
||||
await new Promise<void>((res) => setTimeout(() => res(), 10000));
|
||||
set((state) => addCharge(state, desk, { ...treaty, chad: { glob: null } }));
|
||||
}
|
||||
|
||||
@ -254,4 +254,8 @@ export function allyForTreaty(ship: string, desk: string) {
|
||||
// xx useful for debugging
|
||||
window.docket = useDocketState.getState;
|
||||
|
||||
if (useMockData) {
|
||||
window.desk = 'garden';
|
||||
}
|
||||
|
||||
export default useDocketState;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getVats, Vats, scryLag, getBlockers } from '@urbit/api';
|
||||
import { getVats, Vats, scryLag, getBlockers, Vat } from '@urbit/api';
|
||||
import create from 'zustand';
|
||||
import produce from 'immer';
|
||||
import { useCallback } from 'react';
|
||||
@ -14,7 +14,7 @@ interface KilnState {
|
||||
fetchLag: () => Promise<void>;
|
||||
set: (s: KilnState) => void;
|
||||
}
|
||||
export const useKilnState = create<KilnState>((set) => ({
|
||||
const useKilnState = create<KilnState>((set) => ({
|
||||
vats: useMockData ? mockVats : {},
|
||||
lag: !!useMockData,
|
||||
loaded: false,
|
||||
@ -47,7 +47,7 @@ export function useBlockers() {
|
||||
return useKilnState(selBlockers);
|
||||
}
|
||||
|
||||
export function useVat(desk: string) {
|
||||
export function useVat(desk: string): Vat | undefined {
|
||||
return useKilnState(useCallback((s) => s.vats[desk], [desk]));
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import _ from 'lodash-es';
|
||||
import _ from 'lodash';
|
||||
import { Contact, Contacts } from '@urbit/api';
|
||||
import { Allies, Charges, DocketHrefGlob, Treaties, Treaty } from '@urbit/api/docket';
|
||||
import { Vat, Vats } from '@urbit/api/hood';
|
||||
import systemUrl from '../assets/system.png';
|
||||
@ -18,6 +19,15 @@ export const appMetaData: Pick<Treaty, 'cass' | 'hash' | 'website' | 'license' |
|
||||
const makeHref = (base: string): DocketHrefGlob => ({ glob: { base } });
|
||||
|
||||
export const mockTreaties: Treaties = {
|
||||
'~zod/garden': {
|
||||
ship: '~zod',
|
||||
desk: 'garden',
|
||||
title: 'Landscape',
|
||||
info: 'Your Urbit Home',
|
||||
href: makeHref('garden'),
|
||||
color: '#E2C050',
|
||||
...appMetaData
|
||||
},
|
||||
'~zod/groups': {
|
||||
ship: '~zod',
|
||||
desk: 'groups',
|
||||
@ -155,6 +165,51 @@ export const mockAllies: Allies = [
|
||||
'~nalrys'
|
||||
].reduce((acc, val) => ({ ...acc, [val]: charter }), {});
|
||||
|
||||
const contact: Contact = {
|
||||
nickname: '',
|
||||
bio: '',
|
||||
status: '',
|
||||
color: '#000000',
|
||||
avatar: null,
|
||||
cover: null,
|
||||
groups: [],
|
||||
'last-updated': 0
|
||||
};
|
||||
|
||||
export const mockContacts: Contacts = {
|
||||
'~zod': {
|
||||
...contact,
|
||||
nickname: 'Tlon Corporation'
|
||||
},
|
||||
'~nocsyx-lassul': {
|
||||
...contact,
|
||||
status: 'technomancing an electron wrapper for urbit',
|
||||
color: '#4c00ff'
|
||||
},
|
||||
'~nachus-hollyn': {
|
||||
...contact,
|
||||
avatar: 'https://i.pinimg.com/originals/20/62/59/2062590a440f717a2ae1065ad8e8a4c7.gif'
|
||||
},
|
||||
'~nalbel_litzod': {
|
||||
...contact,
|
||||
nickname: 'Queen'
|
||||
},
|
||||
'~litmus^ritten': {
|
||||
...contact
|
||||
},
|
||||
'~nalput_litzod': {
|
||||
...contact
|
||||
},
|
||||
'~nalrex_bannus': {
|
||||
...contact,
|
||||
status: 'Script, command and inspect your Urbit. Use TUI applications'
|
||||
},
|
||||
'~nalrys': {
|
||||
...contact,
|
||||
status: 'hosting coming soon'
|
||||
}
|
||||
};
|
||||
|
||||
export const mockNotification: BasicNotification = {
|
||||
type: 'basic',
|
||||
time: '',
|
||||
|
98
pkg/grid/src/state/settings.ts
Normal file
98
pkg/grid/src/state/settings.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import {
|
||||
SettingsUpdate,
|
||||
Value,
|
||||
putEntry as doPutEntry,
|
||||
getDeskSettings,
|
||||
DeskData
|
||||
} from '@urbit/api/settings';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
BaseState,
|
||||
createState,
|
||||
createSubscription,
|
||||
pokeOptimisticallyN,
|
||||
reduceStateN
|
||||
} from './base';
|
||||
import api from './api';
|
||||
|
||||
interface BaseSettingsState {
|
||||
display: {
|
||||
theme: 'light' | 'dark' | 'automatic';
|
||||
doNotDisturb: boolean;
|
||||
};
|
||||
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
|
||||
[ref: string]: unknown;
|
||||
}
|
||||
|
||||
export type SettingsState = BaseSettingsState & BaseState<BaseSettingsState>;
|
||||
|
||||
function putBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||
const data = _.get(json, 'put-bucket', false);
|
||||
if (data) {
|
||||
state[data['bucket-key']] = data.bucket;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function delBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||
const data = _.get(json, 'del-bucket', false);
|
||||
if (data) {
|
||||
delete state[data['bucket-key']];
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function putEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||
const data: Record<string, string> = _.get(json, 'put-entry', false);
|
||||
if (data) {
|
||||
if (!state[data['bucket-key']]) {
|
||||
state[data['bucket-key']] = {};
|
||||
}
|
||||
state[data['bucket-key']][data['entry-key']] = data.value;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function delEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||
const data = _.get(json, 'del-entry', false);
|
||||
if (data) {
|
||||
delete state[data['bucket-key']][data['entry-key']];
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export const reduceUpdate = [putBucket, delBucket, putEntry, delEntry];
|
||||
|
||||
export const useSettingsState = createState<BaseSettingsState>(
|
||||
'Settings',
|
||||
(set, get) => ({
|
||||
display: {
|
||||
theme: 'automatic',
|
||||
doNotDisturb: true
|
||||
},
|
||||
loaded: false,
|
||||
putEntry: async (bucket, key, val) => {
|
||||
const poke = doPutEntry(window.desk, bucket, key, val);
|
||||
await pokeOptimisticallyN(useSettingsState, poke, reduceUpdate);
|
||||
},
|
||||
fetchAll: async () => {
|
||||
const result = (await api.scry<DeskData>(getDeskSettings(window.desk))).desk;
|
||||
const newState = {
|
||||
loaded: true,
|
||||
..._.mergeWith(get(), result, (obj, src) => (_.isArray(src) ? src : undefined))
|
||||
};
|
||||
set(newState);
|
||||
}
|
||||
}),
|
||||
[],
|
||||
[
|
||||
(set, get) =>
|
||||
createSubscription('settings-store', `/desk/${window.desk}`, (e) => {
|
||||
const data = _.get(e, 'settings-event', false);
|
||||
if (data) {
|
||||
reduceStateN(get(), data, reduceUpdate);
|
||||
}
|
||||
})
|
||||
]
|
||||
);
|
@ -1,11 +1,5 @@
|
||||
import { DocketHref } from '@urbit/api/docket';
|
||||
|
||||
export function makeKeyFn(key: string) {
|
||||
return (childKeys: string[] = []) => {
|
||||
return [key].concat(childKeys);
|
||||
};
|
||||
}
|
||||
|
||||
export const useMockData = import.meta.env.MODE === 'mock';
|
||||
|
||||
export async function fakeRequest<T>(data: T, time = 300): Promise<T> {
|
||||
@ -23,3 +17,19 @@ export function getAppHref(href: DocketHref) {
|
||||
export function disableDefault<T extends Event>(e: T): void {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// hack until radix-ui fixes this behavior
|
||||
export function handleDropdownLink(setOpen?: (open: boolean) => void): (e: Event) => void {
|
||||
return (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen?.(false), 15);
|
||||
};
|
||||
}
|
||||
|
||||
export function deSig(ship: string): string {
|
||||
if (!ship) {
|
||||
return '';
|
||||
}
|
||||
return ship.replace('~', '');
|
||||
}
|
||||
|
@ -15,5 +15,5 @@
|
||||
}
|
||||
|
||||
.default-ring {
|
||||
@apply focus:ring-4 ring-blue-400 ring-opacity-80 focus:outline-none;
|
||||
}
|
||||
@apply focus-visible:ring-2 ring-blue-400 ring-opacity-80 focus-visible:outline-none;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
.inner-section {
|
||||
@apply p-3 bg-gray-100 rounded-xl;
|
||||
@apply p-3 bg-gray-50 rounded-xl;
|
||||
}
|
||||
|
||||
.input {
|
||||
@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.notification {
|
||||
@apply p-4 bg-gray-100 rounded-xl;
|
||||
@apply p-4 bg-gray-50 rounded-xl;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
|
@ -7,6 +7,7 @@ import { Spinner } from '../components/Spinner';
|
||||
import { getAppHref } from '../state/util';
|
||||
import { useRecentsStore } from '../nav/search/Home';
|
||||
import { ChargeWithDesk } from '../state/docket';
|
||||
import { useTileColor } from './useTileColor';
|
||||
|
||||
type TileProps = {
|
||||
charge: ChargeWithDesk;
|
||||
@ -27,31 +28,34 @@ function getMenuColor(color: string, lightText: boolean, active: boolean): strin
|
||||
|
||||
export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
||||
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||
const { title, color, image, chad, href } = charge;
|
||||
const { title, image, color, chad, href } = charge;
|
||||
const { theme, tileColor } = useTileColor(color);
|
||||
const loading = 'install' in chad;
|
||||
const active = chadIsRunning(chad);
|
||||
const lightText = !readableColorIsBlack(color);
|
||||
const menuColor = getMenuColor(color, lightText, active);
|
||||
const menuColor = getMenuColor(tileColor, theme === 'dark' ? !lightText : lightText, active);
|
||||
const suspendColor = 'rgb(220,220,220)';
|
||||
const suspended = 'suspend' in chad;
|
||||
const link = getAppHref(href);
|
||||
const backgroundColor = active ? tileColor || 'purple' : suspendColor;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={active ? link : undefined}
|
||||
target={desk}
|
||||
className={classNames(
|
||||
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring overflow-hidden',
|
||||
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
|
||||
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800',
|
||||
!active && 'cursor-default'
|
||||
)}
|
||||
style={{ backgroundColor: active ? color || 'purple' : suspendColor }}
|
||||
style={{ backgroundColor }}
|
||||
onClick={() => addRecentApp(charge)}
|
||||
onAuxClick={() => addRecentApp(charge)}
|
||||
>
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2">
|
||||
<Spinner className="h-16 w-16" />
|
||||
<div className="absolute z-10 top-4 left-4 lg:top-8 lg:left-8 flex items-center justify-center">
|
||||
<Spinner className="h-6 w-6" />
|
||||
</div>
|
||||
) : (
|
||||
<TileMenu
|
||||
@ -62,24 +66,17 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
||||
className="absolute z-10 top-2.5 right-2.5 sm:top-4 sm:right-4 opacity-0 hover-none:opacity-100 focus:opacity-100 group-hover:opacity-100"
|
||||
/>
|
||||
)}
|
||||
<div className="h4 absolute z-10 bottom-4 left-4 lg:bottom-8 lg:left-8">
|
||||
<h3
|
||||
className={`${
|
||||
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800'
|
||||
} mix-blend-hard-light`}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<div
|
||||
className="h4 absolute z-10 bottom-3 left-1 lg:bottom-7 lg:left-5 py-1 px-3 rounded-lg"
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
<h3 className="mix-blend-hard-light">{title}</h3>
|
||||
{!active && (
|
||||
<span className="text-gray-400">{suspended ? 'Suspended' : 'Installing'}</span>
|
||||
)}
|
||||
</div>
|
||||
{image && !loading && (
|
||||
<img
|
||||
className="absolute top-1/2 left-1/2 h-full w-full object-contain transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={image}
|
||||
alt=""
|
||||
/>
|
||||
<img className="absolute top-0 left-0 h-full w-full object-contain" src={image} alt="" />
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
|
@ -4,7 +4,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useDocketState from '../state/docket';
|
||||
import { disableDefault } from '../state/util';
|
||||
import { disableDefault, handleDropdownLink } from '../state/util';
|
||||
|
||||
export interface TileMenuProps {
|
||||
desk: string;
|
||||
@ -41,10 +41,7 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
|
||||
const [open, setOpen] = useState(false);
|
||||
const toggleDocket = useDocketState((s) => s.toggleDocket);
|
||||
const menuBg = { backgroundColor: menuColor };
|
||||
const linkOnSelect = useCallback((e: Event) => {
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen(false), 15);
|
||||
}, []);
|
||||
const linkOnSelect = useCallback(handleDropdownLink(setOpen), []);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root open={open} onOpenChange={(isOpen) => setOpen(isOpen)}>
|
||||
@ -64,9 +61,6 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
align="start"
|
||||
alignOffset={-32}
|
||||
sideOffset={4}
|
||||
onCloseAutoFocus={disableDefault}
|
||||
className={classNames(
|
||||
'dropdown py-2 font-semibold',
|
||||
@ -75,15 +69,11 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
|
||||
style={menuBg}
|
||||
>
|
||||
<DropdownMenu.Group>
|
||||
{/*
|
||||
TODO: revisit with Liam
|
||||
<Item as={Link} to={`/leap/search/${provider}/apps/${name.toLowerCase()}`} onSelect={(e) => { e.preventDefault(); setTimeout(() => setOpen(false), 0) }}>App Info</Item>
|
||||
*/}
|
||||
<Item as={Link} to={`/app/${desk}`} onSelect={linkOnSelect}>
|
||||
App Info
|
||||
</Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator className="-mx-4 my-2 border-t-2 border-solid border-gray-500 mix-blend-soft-light" />
|
||||
<DropdownMenu.Separator className="-mx-4 my-2 border-t-2 border-solid border-gray-600 mix-blend-soft-light" />
|
||||
<DropdownMenu.Group>
|
||||
{active && (
|
||||
<Item as={Link} to={`/app/${desk}/suspend`} onSelect={linkOnSelect}>
|
||||
@ -95,10 +85,7 @@ export const TileMenu = ({ desk, active, menuColor, lightText, className }: Tile
|
||||
Remove App
|
||||
</Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Arrow
|
||||
className="w-4 h-[10px] fill-current -translate-x-10"
|
||||
style={{ color: menuColor }}
|
||||
/>
|
||||
<DropdownMenu.Arrow className="w-4 h-[10px] fill-current" style={{ color: menuColor }} />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
|
16
pkg/grid/src/tiles/useTileColor.tsx
Normal file
16
pkg/grid/src/tiles/useTileColor.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { hsla, parseToHsla } from 'color2k';
|
||||
import { usePreferencesStore } from '../nav/preferences/usePreferencesStore';
|
||||
|
||||
function getDarkColor(color: string): string {
|
||||
const hslaColor = parseToHsla(color);
|
||||
return hsla(hslaColor[0], hslaColor[1], 1 - hslaColor[2], 1);
|
||||
}
|
||||
|
||||
export const useTileColor = (color: string) => {
|
||||
const theme = usePreferencesStore((s) => s.currentTheme);
|
||||
|
||||
return {
|
||||
theme,
|
||||
tileColor: theme === 'dark' ? getDarkColor(color) : color
|
||||
};
|
||||
};
|
@ -1,67 +1,214 @@
|
||||
const colors = require('tailwindcss/colors');
|
||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
const resolveConfig = require('tailwindcss/resolveConfig');
|
||||
const { Theme, ThemeManager } = require('tailwindcss-theming/api');
|
||||
|
||||
const themableProperties = [
|
||||
'spacing',
|
||||
'fontFamily',
|
||||
//'fontSize', would require change in tailwindcss-theming
|
||||
'fontWeight',
|
||||
'letterSpacing',
|
||||
'lineHeight',
|
||||
'borderRadius',
|
||||
'borderWidth',
|
||||
'boxShadow'
|
||||
];
|
||||
|
||||
function variablizeTheme(themeConfig, theme) {
|
||||
themableProperties.forEach((prop) => {
|
||||
const propSet = themeConfig[prop];
|
||||
Object.entries(propSet).forEach(([key, value]) => {
|
||||
theme.setVariable(key, value, prop, prop);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const config = resolveConfig({
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'Inter',
|
||||
'Inter UI',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'San Francisco',
|
||||
'Helvetica Neue',
|
||||
'Arial',
|
||||
'sans-serif'
|
||||
],
|
||||
mono: ['Source Code Pro', 'Roboto mono', 'Courier New', 'monospace']
|
||||
},
|
||||
extend: {
|
||||
lineHeight: {
|
||||
tight: 1.2,
|
||||
snug: 1.33334,
|
||||
relaxed: 1.66667
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const base = new Theme().addColors({
|
||||
transparent: 'transparent',
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
gray: {
|
||||
50: '#F2F2F2',
|
||||
100: '#E5E5E5',
|
||||
200: '#CCCCCC',
|
||||
300: '#B3B3B3',
|
||||
400: '#999999',
|
||||
500: '#808080',
|
||||
600: '#666666',
|
||||
700: '#4D4D4D',
|
||||
800: '#333333',
|
||||
900: '#1A1A1A'
|
||||
},
|
||||
blue: {
|
||||
50: '#EFF9FF',
|
||||
100: '#C8EDFF',
|
||||
200: '#A0E1FF',
|
||||
300: '#5FBFFF',
|
||||
400: '#219DFF',
|
||||
500: '#0F75D8',
|
||||
600: '#0252B2',
|
||||
700: '#00388B',
|
||||
800: '#002364',
|
||||
900: '#00133E'
|
||||
},
|
||||
red: {
|
||||
50: '#FFF4F2',
|
||||
100: '#FFDED6',
|
||||
200: '#FFC8B9',
|
||||
300: '#FC9B84',
|
||||
400: '#F57456',
|
||||
500: '#EE5432',
|
||||
600: '#D03B22',
|
||||
700: '#B12918',
|
||||
800: '#931C13',
|
||||
900: '#751410'
|
||||
},
|
||||
orange: {
|
||||
50: '#FFF4EF',
|
||||
100: '#FFE2CE',
|
||||
200: '#FFCEAB',
|
||||
300: '#FFA56F',
|
||||
400: '#FF7E36',
|
||||
500: '#D85E1E',
|
||||
600: '#B2420C',
|
||||
700: '#8B2B00',
|
||||
800: '#641E00',
|
||||
900: '#3E1100'
|
||||
},
|
||||
green: {
|
||||
100: '#E6F5F0',
|
||||
200: '#B3E2D1',
|
||||
300: '#009F65'
|
||||
},
|
||||
yellow: {
|
||||
100: '#FFF9E6',
|
||||
200: '#FFEEB3',
|
||||
300: '#FFDD66',
|
||||
400: '#FFC700'
|
||||
}
|
||||
});
|
||||
variablizeTheme(config.theme, base);
|
||||
|
||||
const dark = new Theme()
|
||||
.setName('dark')
|
||||
.targetable()
|
||||
.addColors({
|
||||
transparent: 'transparent',
|
||||
white: '#000000',
|
||||
black: '#FFFFFF',
|
||||
gray: {
|
||||
50: '#1A1A1A',
|
||||
100: '#333333',
|
||||
200: '#4D4D4D',
|
||||
300: '#666666',
|
||||
400: '#808080',
|
||||
500: '#999999',
|
||||
600: '#B3B3B3',
|
||||
700: '#CCCCCC',
|
||||
800: '#E5E5E5',
|
||||
900: '#F2F2F2'
|
||||
},
|
||||
red: {
|
||||
50: '#751410',
|
||||
100: '#931C13',
|
||||
200: '#B12918',
|
||||
300: '#D03B22',
|
||||
400: '#EE5432',
|
||||
500: '#F57456',
|
||||
600: '#FC9B84',
|
||||
700: '#FFC8B9',
|
||||
800: '#FFDED6',
|
||||
900: '#FFF4F2'
|
||||
},
|
||||
blue: {
|
||||
50: '#00133E',
|
||||
100: '#002364',
|
||||
200: '#00388B',
|
||||
300: '#0252B2',
|
||||
400: '#0F75D8',
|
||||
500: '#219DFF',
|
||||
600: '#5FBFFF',
|
||||
700: '#A0E1FF',
|
||||
800: '#C8EDFF',
|
||||
900: '#EFF9FF'
|
||||
},
|
||||
orange: {
|
||||
50: '#3E1100',
|
||||
100: '#641E00',
|
||||
200: '#8B2B00',
|
||||
300: '#B2420C',
|
||||
400: '#D85E1E',
|
||||
500: '#FF7E36',
|
||||
600: '#FFA56F',
|
||||
700: '#FFCEAB',
|
||||
800: '#FFE2CE',
|
||||
900: '#FFF4EF'
|
||||
},
|
||||
green: {
|
||||
100: '#182722',
|
||||
200: '#134231',
|
||||
300: '#009F65'
|
||||
},
|
||||
yellow: {
|
||||
100: '#312B18',
|
||||
200: '#5F4E13',
|
||||
300: '#A4820B',
|
||||
400: '#FFC700'
|
||||
}
|
||||
});
|
||||
|
||||
const themes = new ThemeManager().setDefaultTheme(base).addTheme(dark);
|
||||
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
darkMode: 'class', // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
gray: {
|
||||
...colors.trueGray,
|
||||
100: '#F2F2F2',
|
||||
200: '#CCCCCC',
|
||||
300: '#B3B3B3',
|
||||
400: '#808080',
|
||||
500: '#666666'
|
||||
},
|
||||
blue: {
|
||||
100: '#E9F5FF',
|
||||
200: '#D3EBFF',
|
||||
300: '#BCE2FF',
|
||||
400: '#219DFF'
|
||||
},
|
||||
red: {
|
||||
100: '#FFF6F5',
|
||||
200: '#FFC6C3',
|
||||
400: '#FF4136'
|
||||
},
|
||||
green: {
|
||||
100: '#E6F5F0',
|
||||
200: '#B3E2D1',
|
||||
400: '#009F65'
|
||||
},
|
||||
yellow: {
|
||||
100: '#FFF9E6',
|
||||
200: '#FFEEB3',
|
||||
300: '#FFDD66',
|
||||
400: '#FFC700'
|
||||
},
|
||||
orange: colors.orange
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'"Inter"',
|
||||
'"Inter UI"',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"San Francisco"',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif'
|
||||
],
|
||||
mono: ['"Source Code Pro"', '"Roboto mono"', '"Courier New"', 'monospace']
|
||||
},
|
||||
minWidth: (theme) => theme('spacing')
|
||||
}
|
||||
},
|
||||
screens: {
|
||||
...defaultTheme.screens,
|
||||
xl: '1440px',
|
||||
'2xl': '2200px'
|
||||
},
|
||||
variants: {
|
||||
extend: {
|
||||
opacity: ['hover-none']
|
||||
}
|
||||
},
|
||||
plugins: [require('@tailwindcss/aspect-ratio'), require('tailwindcss-touch')()]
|
||||
plugins: [
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
require('tailwindcss-touch')(),
|
||||
require('tailwindcss-theming')({
|
||||
themes,
|
||||
strategy: 'class'
|
||||
})
|
||||
]
|
||||
};
|
||||
|
77763
pkg/interface/package-lock.json
generated
77763
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -40,8 +40,8 @@ function delEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||
return state;
|
||||
}
|
||||
|
||||
function getAll(json: any, state: SettingsState): SettingsState {
|
||||
const data = _.get(json, 'all');
|
||||
function getDesk(json: any, state: SettingsState): SettingsState {
|
||||
const data = _.get(json, 'desk');
|
||||
if(data) {
|
||||
_.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined);
|
||||
}
|
||||
@ -75,7 +75,7 @@ export const reduceUpdate = [
|
||||
];
|
||||
|
||||
export const reduceScry = [
|
||||
getAll,
|
||||
getDesk,
|
||||
getBucket,
|
||||
getEntry
|
||||
];
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
import { useCallback } from 'react';
|
||||
import { reduceUpdate } from '../reducers/settings-update';
|
||||
import airlock from '~/logic/api';
|
||||
import { getAll, Value } from '@urbit/api';
|
||||
import { getDeskSettings, Value } from '@urbit/api';
|
||||
import { putEntry } from '@urbit/api/settings';
|
||||
|
||||
export interface ShortcutMapping {
|
||||
@ -45,7 +45,7 @@ export interface SettingsState {
|
||||
keyboard: ShortcutMapping;
|
||||
remoteContentPolicy: RemoteContentPolicy;
|
||||
getAll: () => Promise<void>;
|
||||
putEntry: (bucket: string, key: string, value: Value) => void;
|
||||
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
|
||||
leap: {
|
||||
categories: LeapCategories[];
|
||||
};
|
||||
@ -101,20 +101,22 @@ const useSettingsState = createState<SettingsState>(
|
||||
readGroup: 'shift+Escape'
|
||||
},
|
||||
getAll: async () => {
|
||||
const { all } = await airlock.scry(getAll);
|
||||
const { desk } = await airlock.scry(getDeskSettings((window as any).desk));
|
||||
get().set((s) => {
|
||||
Object.assign(s, all);
|
||||
for(const bucket in desk) {
|
||||
s[bucket] = { ...(s[bucket] || {}), ...desk[bucket] };
|
||||
}
|
||||
});
|
||||
},
|
||||
putEntry: (bucket: string, entry: string, value: Value) => {
|
||||
const poke = putEntry(bucket, entry, value);
|
||||
putEntry: async (bucket: string, entry: string, value: Value) => {
|
||||
const poke = putEntry((window as any).desk, bucket, entry, value);
|
||||
pokeOptimisticallyN(useSettingsState, poke, reduceUpdate);
|
||||
}
|
||||
}),
|
||||
[],
|
||||
[
|
||||
(set, get) =>
|
||||
createSubscription('settings-store', '/all', (e) => {
|
||||
createSubscription('settings-store', `/desk/${(window as any).desk}`, (e) => {
|
||||
const data = _.get(e, 'settings-event', false);
|
||||
if (data) {
|
||||
reduceStateN(get(), data, reduceUpdate);
|
||||
|
@ -32,7 +32,6 @@ import Tiles from './components/tiles';
|
||||
import Tile from './components/tiles/tile';
|
||||
import './css/custom.css';
|
||||
import { join } from '@urbit/api/groups';
|
||||
import { putEntry } from '@urbit/api/settings';
|
||||
import { joinGraph } from '@urbit/api/graph';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
@ -103,15 +102,17 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
|
||||
maxWidth: '350px',
|
||||
modal: function modal(dismiss) {
|
||||
const onDismiss = (e) => {
|
||||
const { putEntry } = useSettingsState.getState();
|
||||
e.stopPropagation();
|
||||
airlock.poke(putEntry('tutorial', 'seen', true));
|
||||
putEntry('tutorial', 'seen', true);
|
||||
dismiss();
|
||||
};
|
||||
const onContinue = async (e) => {
|
||||
const { putEntry } = useSettingsState.getState();
|
||||
e.stopPropagation();
|
||||
if (!hasTutorialGroup({ associations })) {
|
||||
await airlock.poke(join(TUTORIAL_HOST, TUTORIAL_GROUP));
|
||||
await airlock.poke(putEntry('tutorial', 'joined', Date.now()));
|
||||
await putEntry('tutorial', 'joined', Date.now());
|
||||
await waiter(hasTutorialGroup);
|
||||
await Promise.all(
|
||||
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph => airlock.thread(joinGraph(TUTORIAL_HOST, graph))));
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
ManagedCheckboxField, Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { Form, useFormikContext } from 'formik';
|
||||
import { putEntry } from '@urbit/api/settings';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import useSettingsState, { selectSettingsState } from '~/logic/state/settings';
|
||||
@ -15,7 +14,6 @@ import {
|
||||
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
||||
import { ShuffleFields } from '~/views/components/ShuffleFields';
|
||||
import { BackButton } from './BackButton';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
const labels: Record<LeapCategories, string> = {
|
||||
mychannel: 'My Channels',
|
||||
@ -60,11 +58,12 @@ export function LeapSettings() {
|
||||
};
|
||||
|
||||
const onSubmit = async (values: FormSchema) => {
|
||||
const { putEntry } = useSettingsState.getState();
|
||||
const result = values.categories.reduce(
|
||||
(acc, { display, category }) => (display ? [...acc, category] : acc),
|
||||
[] as LeapCategories[]
|
||||
);
|
||||
await airlock.poke(putEntry('leap', 'categories', result));
|
||||
await putEntry('leap', 'categories', result);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -3,7 +3,6 @@ import _ from 'lodash';
|
||||
|
||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||
import { Formik, Form, useField } from 'formik';
|
||||
import { putEntry } from '@urbit/api/settings';
|
||||
|
||||
import { getChord } from '~/logic/lib/util';
|
||||
import useSettingsState, {
|
||||
@ -12,7 +11,6 @@ import useSettingsState, {
|
||||
} from '~/logic/state/settings';
|
||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||
import { BackButton } from './BackButton';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
const settingsSel = selectSettingsState(['keyboard']);
|
||||
|
||||
@ -69,9 +67,10 @@ export default function ShortcutSettings() {
|
||||
initialValues={keyboard}
|
||||
onSubmit={async (values: ShortcutMapping, actions) => {
|
||||
const promises = _.map(values, (value, key) => {
|
||||
const { putEntry } = useSettingsState.getState();
|
||||
return keyboard[key] !== value
|
||||
? airlock.poke(putEntry('keyboard', key, value))
|
||||
: Promise.resolve(0);
|
||||
? putEntry('keyboard', key, value)
|
||||
: Promise.resolve();
|
||||
});
|
||||
await Promise.all(promises);
|
||||
actions.setStatus({ success: null });
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
ManagedTextInputField as Input, Row,
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { join, MetadataUpdatePreview, putEntry } from '@urbit/api';
|
||||
import { join, MetadataUpdatePreview } from '@urbit/api';
|
||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import _ from 'lodash';
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
@ -22,6 +22,7 @@ import { FormError } from '~/views/components/FormError';
|
||||
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
|
||||
import { GroupSummary } from './GroupSummary';
|
||||
import airlock from '~/logic/api';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
|
||||
const formSchema = Yup.object({
|
||||
group: Yup.string()
|
||||
@ -73,8 +74,9 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
|
||||
|
||||
const onConfirm = useCallback(async (group: string) => {
|
||||
const [,,ship,name] = group.split('/');
|
||||
const { putEntry } = useSettingsState.getState();
|
||||
if(group === TUTORIAL_GROUP_RESOURCE) {
|
||||
await airlock.poke(putEntry('tutorial', 'joined', Date.now()));
|
||||
await putEntry('tutorial', 'joined', Date.now());
|
||||
}
|
||||
if (group in groups) {
|
||||
return history.push(`/~landscape${group}`);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Box, Button, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import { leaveGroup, putEntry } from '@urbit/api';
|
||||
import { leaveGroup } from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
@ -16,6 +16,7 @@ import { Portal } from '~/views/components/Portal';
|
||||
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
|
||||
import { Triangle } from '~/views/components/Triangle';
|
||||
import airlock from '~/logic/api';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
|
||||
const localSelector = selectLocalState([
|
||||
'tutorialProgress',
|
||||
@ -94,7 +95,8 @@ export function TutorialModal() {
|
||||
const dismiss = useCallback(async () => {
|
||||
setPaused(false);
|
||||
hideTutorial();
|
||||
await airlock.poke(putEntry('tutorial', 'seen', true));
|
||||
const { putEntry } = useSettingsState.getState();
|
||||
await putEntry('tutorial', 'seen', true);
|
||||
}, [hideTutorial]);
|
||||
|
||||
const bailExit = useCallback(() => {
|
||||
|
@ -35,7 +35,6 @@
|
||||
%observe-hook
|
||||
%s3-store
|
||||
%sane
|
||||
%settings-store
|
||||
%weather
|
||||
==
|
||||
:- %fish
|
||||
|
@ -1,7 +1,7 @@
|
||||
:~ title+'Landscape'
|
||||
info+'A suite of applications to communicate on Urbit'
|
||||
color+0xee.5432
|
||||
glob+'https://bootstrap.urbit.org/glob-0v4.0k6hb.4s38v.su79d.10vd5.7c8lu.glob'
|
||||
glob-http+'https://bootstrap.urbit.org/glob-0v4.0k6hb.4s38v.su79d.10vd5.7c8lu.glob'
|
||||
base+'landscape'
|
||||
version+[0 0 1]
|
||||
website+'https://tlon.io'
|
||||
|
@ -1,31 +0,0 @@
|
||||
|%
|
||||
+$ settings-0 (map key bucket-0)
|
||||
+$ bucket-0 (map key val-0)
|
||||
+$ val-0
|
||||
$% [%s p=@t]
|
||||
[%b p=?]
|
||||
[%n p=@]
|
||||
==
|
||||
::
|
||||
+$ settings (map key bucket)
|
||||
+$ bucket (map key val)
|
||||
+$ key term
|
||||
+$ val
|
||||
$~ [%n 0]
|
||||
$% [%s p=@t]
|
||||
[%b p=?]
|
||||
[%n p=@]
|
||||
[%a p=(list val)]
|
||||
==
|
||||
+$ event
|
||||
$% [%put-bucket =key =bucket]
|
||||
[%del-bucket =key]
|
||||
[%put-entry buc=key =key =val]
|
||||
[%del-entry buc=key =key]
|
||||
==
|
||||
+$ data
|
||||
$% [%all =settings]
|
||||
[%bucket =bucket]
|
||||
[%entry =val]
|
||||
==
|
||||
--
|
1
pkg/landscape/sur/settings.hoon
Symbolic link
1
pkg/landscape/sur/settings.hoon
Symbolic link
@ -0,0 +1 @@
|
||||
../../garden-dev/sur/settings.hoon
|
@ -44,15 +44,16 @@
|
||||
++ read-setting
|
||||
|= key=term
|
||||
=/ m (strand @t) ^- form:m
|
||||
;< =bowl:spider bind:m get-bowl:strandio
|
||||
;< has=? bind:m
|
||||
%+ scry:strandio ?
|
||||
/gx/settings-store/has-entry/gcp-store/[key]/noun
|
||||
/gx/settings-store/has-entry/[q.byk.bowl]/gcp-store/[key]/noun
|
||||
?. has
|
||||
(strand-fail:strandio (rap 3 %gcp-missing- key ~) ~)
|
||||
;< =data:settings bind:m
|
||||
%+ scry:strandio
|
||||
data:settings
|
||||
/gx/settings-store/entry/gcp-store/[key]/settings-data
|
||||
/gx/settings-store/entry/[q.byk.bowl]/gcp-store/[key]/settings-data
|
||||
?> ?=([%entry %s @] data)
|
||||
(pure:m p.val.data)
|
||||
::
|
||||
|
@ -40,9 +40,10 @@ b+has
|
||||
|= key=@tas
|
||||
=/ m (strand ?)
|
||||
^- form:m
|
||||
;< =bowl:spider bind:m get-bowl:strandio
|
||||
;< has=? bind:m
|
||||
%+ scry:strandio ?
|
||||
/gx/settings-store/has-entry/gcp-store/[key]/noun
|
||||
/gx/settings-store/has-entry/[q.byk.bowl]/gcp-store/[key]/noun
|
||||
(pure:m has)
|
||||
::
|
||||
--
|
||||
|
4340
pkg/npm/api/package-lock.json
generated
4340
pkg/npm/api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
import { Poke, Scry } from "../lib";
|
||||
import { Poke, Scry } from '../lib';
|
||||
import { PutBucket, Key, Bucket, DelBucket, Value, PutEntry, DelEntry, SettingsUpdate } from './types';
|
||||
|
||||
export const action = <T extends SettingsUpdate>(data: T): Poke<T> => ({
|
||||
@ -8,29 +8,35 @@ export const action = <T extends SettingsUpdate>(data: T): Poke<T> => ({
|
||||
});
|
||||
|
||||
export const putBucket = (
|
||||
desk: string,
|
||||
key: Key,
|
||||
bucket: Bucket
|
||||
): Poke<PutBucket> => action({
|
||||
'put-bucket': {
|
||||
desk,
|
||||
'bucket-key': key,
|
||||
'bucket': bucket
|
||||
}
|
||||
});
|
||||
|
||||
export const delBucket = (
|
||||
desk: string,
|
||||
key: Key
|
||||
): Poke<DelBucket> => action({
|
||||
'del-bucket': {
|
||||
desk,
|
||||
'bucket-key': key
|
||||
}
|
||||
});
|
||||
|
||||
export const putEntry = (
|
||||
desk: string,
|
||||
bucket: Key,
|
||||
key: Key,
|
||||
value: Value
|
||||
): Poke<PutEntry> => action({
|
||||
'put-entry': {
|
||||
desk,
|
||||
'bucket-key': bucket,
|
||||
'entry-key': key,
|
||||
value: value
|
||||
@ -38,10 +44,12 @@ export const putEntry = (
|
||||
});
|
||||
|
||||
export const delEntry = (
|
||||
desk: string,
|
||||
bucket: Key,
|
||||
key: Key
|
||||
): Poke<DelEntry> => action({
|
||||
'del-entry': {
|
||||
desk,
|
||||
'bucket-key': bucket,
|
||||
'entry-key': key
|
||||
}
|
||||
@ -50,17 +58,21 @@ export const delEntry = (
|
||||
export const getAll: Scry = {
|
||||
app: 'settings-store',
|
||||
path: '/all'
|
||||
}
|
||||
};
|
||||
|
||||
export const getBucket = (bucket: string) => ({
|
||||
export const getBucket = (desk: string, bucket: string) => ({
|
||||
app: 'settings-store',
|
||||
path: `/bucket/${bucket}`
|
||||
});
|
||||
|
||||
export const getEntry = (bucket: string, entry: string) => ({
|
||||
export const getEntry = (desk: string, bucket: string, entry: string) => ({
|
||||
app: 'settings-store',
|
||||
path: `/entry/${bucket}/${entry}`
|
||||
path: `/entry/${desk}/${bucket}/${entry}`
|
||||
});
|
||||
|
||||
export const getDeskSettings = (desk: string) => ({
|
||||
app: 'settings-store',
|
||||
path: `/desk/${desk}`
|
||||
});
|
||||
|
||||
|
||||
export * from './types';
|
||||
|
@ -1,46 +1,54 @@
|
||||
export type Key = string;
|
||||
export type Value = string | string[] | boolean | number;
|
||||
export type Bucket = Map<string, Value>;
|
||||
export type Settings = Map<string, Bucket>;
|
||||
export type Bucket = { [key: string]: Value; };
|
||||
export type DeskSettings = { [bucket: string]: Bucket; };
|
||||
export type Settings = { [desk: string]: Settings; }
|
||||
|
||||
export interface PutBucket {
|
||||
"put-bucket": {
|
||||
"bucket-key": Key;
|
||||
"bucket": Bucket;
|
||||
'put-bucket': {
|
||||
desk: string;
|
||||
'bucket-key': Key;
|
||||
'bucket': Bucket;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DelBucket {
|
||||
"del-bucket": {
|
||||
"bucket-key": Key;
|
||||
'del-bucket': {
|
||||
desk: string;
|
||||
'bucket-key': Key;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PutEntry {
|
||||
"put-entry": {
|
||||
"bucket-key": Key;
|
||||
"entry-key": Key;
|
||||
"value"?: Value;
|
||||
'put-entry': {
|
||||
'bucket-key': Key;
|
||||
'entry-key': Key;
|
||||
'value'?: Value;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DelEntry {
|
||||
"del-entry": {
|
||||
"bucket-key": Key;
|
||||
"entry-key": Key;
|
||||
'del-entry': {
|
||||
desk: string;
|
||||
'bucket-key': Key;
|
||||
'entry-key': Key;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AllData {
|
||||
"all": Settings;
|
||||
'all': Settings;
|
||||
}
|
||||
|
||||
export interface DeskData {
|
||||
desk: DeskSettings;
|
||||
}
|
||||
|
||||
export interface BucketData {
|
||||
"bucket": Bucket;
|
||||
'bucket': Bucket;
|
||||
}
|
||||
|
||||
export interface EntryData {
|
||||
"entry": Value;
|
||||
'entry': Value;
|
||||
}
|
||||
|
||||
export type SettingsUpdate =
|
||||
@ -52,4 +60,5 @@ export type SettingsUpdate =
|
||||
export type SettingsData =
|
||||
| AllData
|
||||
| BucketData
|
||||
| EntryData;
|
||||
| EntryData
|
||||
| DeskData;
|
||||
|
6052
pkg/npm/eslint-config/package-lock.json
generated
6052
pkg/npm/eslint-config/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user