@profullstack/agenticjobs 0.11.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +136 -1
  2. package/dist/cli/args.js +4 -2
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/index.js +227 -5
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/jobfile.js +38 -2
  7. package/dist/cli/jobfile.js.map +1 -1
  8. package/dist/client/client.d.ts +157 -0
  9. package/dist/client/client.js +44 -0
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/config.d.ts +35 -1
  12. package/dist/config.js +33 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agentwriter.js +22 -5
  15. package/dist/core/agentwriter.js.map +1 -1
  16. package/dist/core/applications.js +1 -1
  17. package/dist/core/applications.js.map +1 -1
  18. package/dist/core/candidates.js +3 -5
  19. package/dist/core/candidates.js.map +1 -1
  20. package/dist/core/capacity.d.ts +1 -1
  21. package/dist/core/capacity.js +11 -19
  22. package/dist/core/capacity.js.map +1 -1
  23. package/dist/core/coinpay.d.ts +143 -0
  24. package/dist/core/coinpay.js +417 -0
  25. package/dist/core/coinpay.js.map +1 -0
  26. package/dist/core/import.js +19 -9
  27. package/dist/core/import.js.map +1 -1
  28. package/dist/core/inbox.d.ts +155 -0
  29. package/dist/core/inbox.js +353 -0
  30. package/dist/core/inbox.js.map +1 -0
  31. package/dist/core/invoices.d.ts +111 -0
  32. package/dist/core/invoices.js +292 -0
  33. package/dist/core/invoices.js.map +1 -0
  34. package/dist/core/jobs.d.ts +24 -6
  35. package/dist/core/jobs.js +116 -31
  36. package/dist/core/jobs.js.map +1 -1
  37. package/dist/core/mail.d.ts +16 -0
  38. package/dist/core/mail.js +22 -0
  39. package/dist/core/mail.js.map +1 -1
  40. package/dist/core/recommendations.d.ts +134 -0
  41. package/dist/core/recommendations.js +356 -0
  42. package/dist/core/recommendations.js.map +1 -0
  43. package/dist/core/updates.d.ts +15 -3
  44. package/dist/core/updates.js +58 -24
  45. package/dist/core/updates.js.map +1 -1
  46. package/dist/core/zip.js +29 -4
  47. package/dist/core/zip.js.map +1 -1
  48. package/dist/directory/federate.js +3 -1
  49. package/dist/directory/federate.js.map +1 -1
  50. package/dist/directory/fetch.d.ts +1 -1
  51. package/dist/markup/markdown.js +6 -2
  52. package/dist/markup/markdown.js.map +1 -1
  53. package/dist/markup/resume.js +18 -5
  54. package/dist/markup/resume.js.map +1 -1
  55. package/dist/mcp/tools.js +249 -7
  56. package/dist/mcp/tools.js.map +1 -1
  57. package/dist/schema/index.d.ts +1 -0
  58. package/dist/schema/index.js +1 -0
  59. package/dist/schema/index.js.map +1 -1
  60. package/dist/schema/instance.js +29 -0
  61. package/dist/schema/instance.js.map +1 -1
  62. package/dist/schema/job.d.ts +12 -0
  63. package/dist/schema/job.js +17 -3
  64. package/dist/schema/job.js.map +1 -1
  65. package/dist/schema/jsonld.js +7 -6
  66. package/dist/schema/jsonld.js.map +1 -1
  67. package/dist/schema/pay.d.ts +173 -0
  68. package/dist/schema/pay.js +668 -0
  69. package/dist/schema/pay.js.map +1 -0
  70. package/dist/server/app.d.ts +4 -3
  71. package/dist/server/app.js +10 -5
  72. package/dist/server/app.js.map +1 -1
  73. package/dist/server/deps.d.ts +5 -0
  74. package/dist/server/middleware.d.ts +11 -1
  75. package/dist/server/middleware.js +26 -3
  76. package/dist/server/middleware.js.map +1 -1
  77. package/dist/server/routes/api.js +437 -7
  78. package/dist/server/routes/api.js.map +1 -1
  79. package/dist/server/routes/discovery.js +29 -8
  80. package/dist/server/routes/discovery.js.map +1 -1
  81. package/dist/server/routes/inbox.d.ts +9 -0
  82. package/dist/server/routes/inbox.js +223 -0
  83. package/dist/server/routes/inbox.js.map +1 -0
  84. package/dist/server/routes/openapi.js +260 -2
  85. package/dist/server/routes/openapi.js.map +1 -1
  86. package/dist/server/routes/pages.d.ts +22 -0
  87. package/dist/server/routes/pages.js +244 -19
  88. package/dist/server/routes/pages.js.map +1 -1
  89. package/dist/tui/views.js +9 -3
  90. package/dist/tui/views.js.map +1 -1
  91. package/dist/views/candidates.d.ts +7 -0
  92. package/dist/views/candidates.js +3 -2
  93. package/dist/views/candidates.js.map +1 -1
  94. package/dist/views/docs.js +10 -1
  95. package/dist/views/docs.js.map +1 -1
  96. package/dist/views/inbox.d.ts +90 -0
  97. package/dist/views/inbox.js +73 -0
  98. package/dist/views/inbox.js.map +1 -0
  99. package/dist/views/jobs.d.ts +25 -0
  100. package/dist/views/jobs.js +35 -12
  101. package/dist/views/jobs.js.map +1 -1
  102. package/dist/views/layout.d.ts +24 -0
  103. package/dist/views/layout.js +20 -2
  104. package/dist/views/layout.js.map +1 -1
  105. package/dist/views/me.d.ts +10 -3
  106. package/dist/views/me.js +3 -2
  107. package/dist/views/me.js.map +1 -1
  108. package/dist/views/post.d.ts +18 -0
  109. package/dist/views/post.js +39 -7
  110. package/dist/views/post.js.map +1 -1
  111. package/dist/views/recommendations.d.ts +43 -0
  112. package/dist/views/recommendations.js +26 -0
  113. package/dist/views/recommendations.js.map +1 -0
  114. package/dist/views/updates.d.ts +13 -0
  115. package/dist/views/updates.js +6 -5
  116. package/dist/views/updates.js.map +1 -1
  117. package/docs/openjob.md +63 -0
  118. package/migrations/0014_inbox_and_billing.sql +138 -0
  119. package/migrations/0015_pay_lines.sql +38 -0
  120. package/migrations/0016_recommendations.sql +65 -0
  121. package/package.json +1 -1
  122. package/web/public/app.css +192 -2
  123. package/web/public/sw.js +1 -1
@@ -0,0 +1,138 @@
1
+ -- The inbox, and the invoices that travel through it.
2
+ --
3
+ -- A board where the only way to reach somebody is a public comment box gets
4
+ -- used as one: ugig.net's comments filled up with invoices, because an invoice
5
+ -- has to go *somewhere* and that was the only somewhere. So there is no public
6
+ -- commenting here at all. A conversation is private to the people in it, and
7
+ -- an invoice is a message in that conversation with money attached.
8
+ --
9
+ -- Two things are in the schema rather than in a validator:
10
+ --
11
+ -- - A message is bounded. 4000 characters is a long letter; it is not room
12
+ -- for a pasted document, which belongs on a resume or a listing.
13
+ -- - An invoice has one payee and one amount, and its status moves forward.
14
+ -- It is never edited after it is sent: a changed invoice is a new one.
15
+
16
+ -- A conversation. The subject is the one line the list page shows.
17
+ create table if not exists threads (
18
+ id uuid primary key default gen_random_uuid(),
19
+ subject text not null check (length(subject) between 1 and 140),
20
+ -- The listing this is about, when it is about one. Kept when the listing
21
+ -- is deleted: the conversation still happened.
22
+ job_id uuid references jobs (id) on delete set null,
23
+ created_by uuid not null references users (id) on delete cascade,
24
+ created_at timestamptz not null default now(),
25
+ -- Denormalised so the inbox sorts without touching messages.
26
+ last_message_at timestamptz not null default now()
27
+ );
28
+
29
+ -- Who is in it. A person is in a conversation either as themselves or as a
30
+ -- member of an employer; `org_id` says which, and is what the other side sees
31
+ -- as the name. Every member of the employer is a participant, so a message to
32
+ -- "Acme" reaches whoever at Acme is around.
33
+ create table if not exists thread_participants (
34
+ thread_id uuid not null references threads (id) on delete cascade,
35
+ user_id uuid not null references users (id) on delete cascade,
36
+ org_id uuid references organisations (id) on delete set null,
37
+ -- Unread is "a message newer than this". Null means never opened.
38
+ last_read_at timestamptz,
39
+ -- When this person was last emailed about this thread, so a burst of ten
40
+ -- messages is one email rather than ten.
41
+ notified_at timestamptz,
42
+ primary key (thread_id, user_id)
43
+ );
44
+
45
+ create index if not exists thread_participants_user_idx on thread_participants (user_id);
46
+ create index if not exists thread_participants_org_idx on thread_participants (org_id)
47
+ where org_id is not null;
48
+ create index if not exists threads_job_idx on threads (job_id) where job_id is not null;
49
+
50
+ -- An invoice. Its money is settled on CoinPay, to the payee's own wallet; the
51
+ -- board never holds funds. The row here is the source of truth for *which*
52
+ -- conversation it belongs to and *whether it has been paid*; the CoinPay
53
+ -- payment is the source of truth for the chain-level facts, and is re-minted
54
+ -- when a quote expires unpaid, so `coinpay_payment_id` changes over time and
55
+ -- `status` does not move backwards.
56
+ create table if not exists invoices (
57
+ id uuid primary key default gen_random_uuid(),
58
+ thread_id uuid not null references threads (id) on delete cascade,
59
+ -- Who gets paid. Always a person: a payout goes to a wallet somebody
60
+ -- connected, and an employer does not have one.
61
+ payee_id uuid not null references users (id) on delete cascade,
62
+ amount_usd numeric(12, 2) not null check (amount_usd > 0),
63
+ -- The chain the payee chose to be paid on, in CoinPay's spelling:
64
+ -- BTC, ETH, SOL, USDC_POL and so on.
65
+ currency text not null,
66
+ -- The payee's address for that chain, copied at send time. An invoice
67
+ -- settles to the wallet it was sent with, whatever is connected later.
68
+ wallet_address text not null,
69
+ description text not null default '' check (length(description) <= 1000),
70
+ status text not null default 'sent'
71
+ check (status in ('sent', 'paid', 'cancelled')),
72
+ coinpay_payment_id text,
73
+ payment_address text,
74
+ amount_crypto text,
75
+ payment_expires_at timestamptz,
76
+ paid_at timestamptz,
77
+ paid_by uuid references users (id) on delete set null,
78
+ tx_hash text,
79
+ created_at timestamptz not null default now(),
80
+ updated_at timestamptz not null default now()
81
+ );
82
+
83
+ create index if not exists invoices_thread_idx on invoices (thread_id, created_at desc);
84
+ create index if not exists invoices_payee_idx on invoices (payee_id, created_at desc);
85
+ -- The webhook looks a payment up by CoinPay's id and nothing else.
86
+ create unique index if not exists invoices_payment_key on invoices (coinpay_payment_id)
87
+ where coinpay_payment_id is not null;
88
+
89
+ -- A message. `kind` says whether it is words or an invoice; an invoice message
90
+ -- carries the invoice and its body is the description, so a thread reads
91
+ -- top to bottom without joining.
92
+ create table if not exists messages (
93
+ id uuid primary key default gen_random_uuid(),
94
+ thread_id uuid not null references threads (id) on delete cascade,
95
+ sender_id uuid not null references users (id) on delete cascade,
96
+ kind text not null default 'text' check (kind in ('text', 'invoice')),
97
+ body text not null check (length(body) between 1 and 4000),
98
+ invoice_id uuid references invoices (id) on delete set null,
99
+ created_at timestamptz not null default now(),
100
+ constraint messages_invoice_kind check (
101
+ (kind = 'invoice' and invoice_id is not null) or (kind = 'text' and invoice_id is null)
102
+ )
103
+ );
104
+
105
+ create index if not exists messages_thread_idx on messages (thread_id, created_at);
106
+
107
+ -- A CoinPay account somebody connected, so the board can read which wallets
108
+ -- they can be paid to. The tokens are theirs, for their account: the board
109
+ -- reads `userinfo` with them and nothing else.
110
+ --
111
+ -- One CoinPay account attaches to one person here, and the unique index is
112
+ -- what says so. Without it the first account to connect a wallet would be the
113
+ -- one every later profile quietly pays out to.
114
+ create table if not exists coinpay_accounts (
115
+ user_id uuid primary key references users (id) on delete cascade,
116
+ coinpay_sub text not null unique,
117
+ email text,
118
+ name text,
119
+ access_token text not null,
120
+ refresh_token text,
121
+ scope text not null default '',
122
+ expires_at timestamptz not null,
123
+ -- What userinfo last said: [{ address, chain, label }]. Refreshed on
124
+ -- connect and on request, never trusted to be more current than that.
125
+ wallets jsonb not null default '[]'::jsonb,
126
+ connected_at timestamptz not null default now(),
127
+ updated_at timestamptz not null default now()
128
+ );
129
+
130
+ -- An authorization in flight. Server-side rather than in a cookie so the same
131
+ -- flow works from a terminal, and so a state is single-use by deletion.
132
+ create table if not exists coinpay_oauth_states (
133
+ state text primary key,
134
+ user_id uuid not null references users (id) on delete cascade,
135
+ code_verifier text not null,
136
+ redirect text,
137
+ expires_at timestamptz not null
138
+ );
@@ -0,0 +1,38 @@
1
+ -- What a listing pays, in full.
2
+ --
3
+ -- A salary range per year was the whole vocabulary, and it described one
4
+ -- kind of work. The listings this board exists for pay per task, per pull
5
+ -- request, per social post, a flat fee for a project, a share of revenue or
6
+ -- a bounty, in dollars or in a coin, settled over whatever rail the two sides
7
+ -- agree on. This is the same model ugig.net carries as budget_type,
8
+ -- budget_unit and payment_coin, so a gig there and a job here describe pay
9
+ -- the same way.
10
+ --
11
+ -- The lines are JSON rather than a child table because they are read and
12
+ -- written as one value with the listing, never queried on their own: the
13
+ -- salary_* columns stay as the flattened first time-based line, which is
14
+ -- what the salary filter, the salary sort and the JobPosting structured
15
+ -- data compare on. A price per task has no annual figure and does not belong
16
+ -- in any of those.
17
+ alter table jobs add column if not exists pay_lines jsonb not null default '[]'::jsonb;
18
+ -- How it is settled: a coin such as SOL, or a rail such as "bank transfer".
19
+ alter table jobs add column if not exists pay_method text;
20
+
21
+ -- Every listing that already stated a range becomes one line, so the page
22
+ -- and the API say the same thing before and after this migration.
23
+ update jobs
24
+ set pay_lines = jsonb_build_array(jsonb_build_object(
25
+ 'type', case salary_period
26
+ when 'hour' then 'hourly'
27
+ when 'day' then 'daily'
28
+ when 'week' then 'weekly'
29
+ when 'month' then 'monthly'
30
+ else 'yearly'
31
+ end,
32
+ 'min', salary_min,
33
+ 'max', salary_max,
34
+ 'currency', upper(coalesce(salary_currency, 'USD')),
35
+ 'unit', null))
36
+ where pay_lines = '[]'::jsonb
37
+ and salary_unpaid = false
38
+ and (salary_min is not null or salary_max is not null);
@@ -0,0 +1,65 @@
1
+ -- Recommendations: what somebody who worked with you says about you, shown
2
+ -- on your page once you have agreed to it.
3
+ --
4
+ -- Not a rating. A star out of five from a stranger who paid you once is a
5
+ -- number that says nothing and cannot be answered; it is the thing that
6
+ -- makes a marketplace profile a scoreboard. A recommendation is a paragraph
7
+ -- with a name on it, from an employer who hired you or a person you hired,
8
+ -- and it goes on your page only when you approve it. You can reject one,
9
+ -- and nothing is shown. That is LinkedIn's model, and it is the right one
10
+ -- for a board where every profile is a person's own document.
11
+ --
12
+ -- Both directions, because the board is symmetric: an employer recommends a
13
+ -- candidate, and a candidate recommends an employer. Each side is a person
14
+ -- or an employer, the same way a message is.
15
+ create table if not exists recommendations (
16
+ id uuid primary key default gen_random_uuid(),
17
+ -- Who is saying it: a person as themselves, or an employer through a
18
+ -- member. Exactly one.
19
+ author_user_id uuid references users (id) on delete cascade,
20
+ author_org_id uuid references organisations (id) on delete cascade,
21
+ -- The person who typed it, kept even for an employer's, so "who wrote
22
+ -- this" has an answer after they leave the company.
23
+ written_by uuid not null references users (id) on delete cascade,
24
+ -- Who it is about. Exactly one.
25
+ user_id uuid references users (id) on delete cascade,
26
+ org_id uuid references organisations (id) on delete cascade,
27
+ -- The words. Text, rendered as text.
28
+ body text not null check (length(body) between 20 and 2000),
29
+ -- "Hired them for a three-month contract", "Worked with them at Acme".
30
+ relationship text,
31
+ -- pending until the subject approves; approved is public; rejected is
32
+ -- kept so the author can see it was, and so it cannot be re-sent daily.
33
+ status text not null default 'pending'
34
+ check (status in ('pending', 'approved', 'rejected')),
35
+ created_at timestamptz not null default now(),
36
+ updated_at timestamptz not null default now(),
37
+ decided_at timestamptz,
38
+ constraint recommendations_one_author check (
39
+ (author_user_id is null) <> (author_org_id is null)
40
+ ),
41
+ constraint recommendations_one_subject check ((user_id is null) <> (org_id is null)),
42
+ -- Nobody recommends themselves, and an employer does not recommend itself.
43
+ constraint recommendations_not_self check (
44
+ (author_user_id is null or user_id is null or author_user_id <> user_id)
45
+ and (author_org_id is null or org_id is null or author_org_id <> org_id)
46
+ )
47
+ );
48
+
49
+ -- One per author per subject. Writing it again replaces the words and goes
50
+ -- back to pending for the subject to read again.
51
+ create unique index if not exists recommendations_user_user_key
52
+ on recommendations (author_user_id, user_id) where author_user_id is not null and user_id is not null;
53
+ create unique index if not exists recommendations_user_org_key
54
+ on recommendations (author_user_id, org_id) where author_user_id is not null and org_id is not null;
55
+ create unique index if not exists recommendations_org_user_key
56
+ on recommendations (author_org_id, user_id) where author_org_id is not null and user_id is not null;
57
+ create unique index if not exists recommendations_org_org_key
58
+ on recommendations (author_org_id, org_id) where author_org_id is not null and org_id is not null;
59
+
60
+ -- A page reads "approved, newest first" for one subject.
61
+ create index if not exists recommendations_user_idx on recommendations (user_id, status, decided_at desc)
62
+ where user_id is not null;
63
+ create index if not exists recommendations_org_idx on recommendations (org_id, status, decided_at desc)
64
+ where org_id is not null;
65
+ create index if not exists recommendations_written_idx on recommendations (written_by, created_at desc);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/agenticjobs",
3
- "version": "0.11.0",
3
+ "version": "0.14.0",
4
4
  "description": "An agent-friendly job board you self-host. It posts its own jobs, never scrapes anyone else's, and answers on every surface: web, API, MCP, CLI, TUI, desktop and PWA. Instances find each other through an open directory.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -185,6 +185,33 @@ main {
185
185
  }
186
186
  }
187
187
 
188
+ /*
189
+ * The sidebar is a fixed 20rem track, and what goes in it is exactly the kind
190
+ * of text that cannot break on its own: an email address, a URL, the API line
191
+ * an agent is meant to copy, a skill tag. With no wrapping rule each one is a
192
+ * single word wider than the card, and it walks out over the card's border
193
+ * (the candidate page's contact block did). `anywhere` lets these break at
194
+ * any character; it inherits, so one rule on the aside covers every card.
195
+ *
196
+ * A code block in the sidebar wraps instead of scrolling. A 20rem scroller
197
+ * shows "GET https://example.com/api/v1/cand" and hides the part that
198
+ * matters; the wrapped line is still one line to copy. The page-wide
199
+ * `.code-block` rule keeps its `overflow-x: auto` for the docs, where lines
200
+ * are meant to stay lines.
201
+ */
202
+ .grid-2 > aside {
203
+ overflow-wrap: anywhere;
204
+ }
205
+
206
+ .grid-2 > aside .code-block {
207
+ white-space: pre-wrap;
208
+ }
209
+
210
+ .grid-2 > aside .badge {
211
+ white-space: normal;
212
+ text-align: left;
213
+ }
214
+
188
215
  .stack {
189
216
  display: flex;
190
217
  flex-direction: column;
@@ -723,6 +750,41 @@ a.badge-plain:hover .badge {
723
750
  font-weight: 600;
724
751
  }
725
752
 
753
+ /* A body inside a card: a message, an update, a recommendation, an answer.
754
+ Same grammar as a listing, tighter spacing, and no heading taller than the
755
+ card's own title. The last block loses its bottom margin so the meta line
756
+ underneath sits where it did when the body was one paragraph. */
757
+ .prose-compact {
758
+ max-width: none;
759
+ }
760
+
761
+ .prose-compact p,
762
+ .prose-compact ul,
763
+ .prose-compact ol,
764
+ .prose-compact blockquote,
765
+ .prose-compact pre,
766
+ .prose-compact .table-wrap {
767
+ margin: 0 0 0.6rem;
768
+ }
769
+
770
+ .prose-compact > :last-child {
771
+ margin-bottom: 0;
772
+ }
773
+
774
+ .prose-compact h1,
775
+ .prose-compact h2,
776
+ .prose-compact h3,
777
+ .prose-compact h4,
778
+ .prose-compact h5,
779
+ .prose-compact h6 {
780
+ font-size: 1.05em;
781
+ margin: 0.9rem 0 0.3rem;
782
+ }
783
+
784
+ .prose-compact hr {
785
+ margin: 0.9rem 0;
786
+ }
787
+
726
788
  /* --- misc -------------------------------------------------------------- */
727
789
 
728
790
  .code-block {
@@ -857,8 +919,6 @@ a.badge-plain:hover .badge {
857
919
 
858
920
  .update-body {
859
921
  margin: 0;
860
- /* Written in a textarea, so its line breaks are the ones the author made. */
861
- white-space: pre-wrap;
862
922
  /* A pasted URL is one unbreakable word and would otherwise push the card
863
923
  wider than the viewport on a phone. */
864
924
  overflow-wrap: anywhere;
@@ -867,3 +927,133 @@ a.badge-plain:hover .badge {
867
927
  .update-link {
868
928
  overflow-wrap: anywhere;
869
929
  }
930
+
931
+ /* --- inbox ------------------------------------------------------------- */
932
+
933
+ .nav-count {
934
+ display: inline-block;
935
+ min-width: 1.3em;
936
+ padding: 0 0.35em;
937
+ border-radius: 999px;
938
+ background: var(--primary);
939
+ color: var(--primary-foreground);
940
+ font-size: 0.75em;
941
+ line-height: 1.6;
942
+ text-align: center;
943
+ }
944
+
945
+ .thread-list,
946
+ .message-list {
947
+ list-style: none;
948
+ margin: 0;
949
+ padding: 0;
950
+ display: flex;
951
+ flex-direction: column;
952
+ gap: 0.75rem;
953
+ }
954
+
955
+ .thread-subject {
956
+ margin: 0.35rem 0 0.2rem;
957
+ font-weight: 600;
958
+ }
959
+
960
+ .thread-preview {
961
+ margin: 0;
962
+ overflow-wrap: anywhere;
963
+ }
964
+
965
+ .message {
966
+ border: 1px solid var(--border);
967
+ border-radius: var(--radius);
968
+ padding: 0.9rem 1rem;
969
+ background: var(--card);
970
+ max-width: 92%;
971
+ scroll-margin-top: 5rem;
972
+ }
973
+
974
+ /* Yours sit to the right, theirs to the left, so a long thread scans. */
975
+ .message.mine {
976
+ align-self: flex-end;
977
+ background: color-mix(in oklab, var(--primary) 8%, var(--card));
978
+ }
979
+
980
+ .message-meta {
981
+ margin: 0 0 0.35rem;
982
+ }
983
+
984
+ .message-body {
985
+ margin: 0;
986
+ overflow-wrap: anywhere;
987
+ }
988
+
989
+ .invoice {
990
+ border: 1px dashed var(--border);
991
+ border-radius: calc(var(--radius) - 2px);
992
+ padding: 0.75rem 0.9rem;
993
+ display: flex;
994
+ flex-direction: column;
995
+ gap: 0.5rem;
996
+ /* Addresses and transaction hashes are one word each. */
997
+ overflow-wrap: anywhere;
998
+ }
999
+
1000
+ .invoice-amount {
1001
+ font-size: 1.25rem;
1002
+ font-weight: 700;
1003
+ }
1004
+
1005
+ .invoice-description {
1006
+ margin: 0;
1007
+ overflow-wrap: anywhere;
1008
+ }
1009
+
1010
+ .invoice-form > summary {
1011
+ cursor: pointer;
1012
+ }
1013
+
1014
+ .wallet-list code {
1015
+ overflow-wrap: anywhere;
1016
+ }
1017
+
1018
+ .table {
1019
+ width: 100%;
1020
+ border-collapse: collapse;
1021
+ font-size: 0.9rem;
1022
+ }
1023
+
1024
+ .table th,
1025
+ .table td {
1026
+ text-align: left;
1027
+ padding: 0.5rem 0.6rem;
1028
+ border-bottom: 1px solid var(--border);
1029
+ vertical-align: top;
1030
+ }
1031
+
1032
+ .table th {
1033
+ color: var(--muted-foreground);
1034
+ font-weight: 500;
1035
+ }
1036
+
1037
+ /* --- recommendations --------------------------------------------------- */
1038
+
1039
+ .recommendations {
1040
+ list-style: none;
1041
+ margin: 0;
1042
+ padding: 0;
1043
+ display: flex;
1044
+ flex-direction: column;
1045
+ gap: 0.9rem;
1046
+ }
1047
+
1048
+ .recommendation {
1049
+ padding-left: 0.9rem;
1050
+ border-left: 3px solid color-mix(in oklab, var(--primary) 45%, var(--border));
1051
+ }
1052
+
1053
+ .recommendation-body {
1054
+ overflow-wrap: anywhere;
1055
+ }
1056
+
1057
+ .recommendation-meta {
1058
+ margin: 0.35rem 0 0;
1059
+ }
package/web/public/sw.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * drifted, which is the only version of this rule that survives contact.
14
14
  */
15
15
 
16
- const VERSION = '0313cc56';
16
+ const VERSION = 'bb197f9b';
17
17
  const SHELL = `shell-${VERSION}`;
18
18
  const ASSETS = ['/assets/app.css', '/assets/tokens.css', '/assets/icon.svg'];
19
19