@anthusai/papyrus 5.42.1 → 5.43.0-next.2
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.
- package/amplify/data/schema.js +2 -0
- package/amplify/data/schema.ts +2 -0
- package/amplify/functions/monitoring-view/schema.js +6 -0
- package/amplify/functions/monitoring-view/schema.ts +6 -0
- package/amplify/functions/monitoring-view/wire.js +1 -0
- package/amplify/functions/monitoring-view/wire.ts +1 -0
- package/amplify/functions/proposal-actions/handler.py +137 -0
- package/amplify/functions/proposal-actions/resource.js +15 -0
- package/amplify/functions/proposal-actions/resource.ts +22 -0
- package/amplify/functions/proposal-actions/schema.js +106 -0
- package/amplify/functions/proposal-actions/schema.ts +108 -0
- package/amplify/functions/proposal-actions/wire.js +10 -0
- package/amplify/functions/proposal-actions/wire.ts +13 -0
- package/amplify/plugins/reader-steering/function/handler.py +12 -69
- package/amplify/plugins/reader-steering/papyrus.plugin.js +5 -5
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +5 -5
- package/amplify/plugins/reader-steering/schema.js +0 -95
- package/amplify/plugins/reader-steering/schema.ts +0 -95
- package/amplify/site-backend.js +4 -0
- package/amplify/site-backend.ts +4 -0
- package/app/.well-known/papyrus-cli.json/route.ts +9 -0
- package/app/globals.css +31 -35
- package/app/tailwind.css +2 -0
- package/components/monitoring-overview-view.tsx +12 -7
- package/components/monitoring-panel.tsx +33 -3
- package/components/newsroom-mobile-navigation.tsx +23 -0
- package/components/newsroom-ops-shell.tsx +8 -10
- package/components/newsroom-organizer.css +1 -0
- package/components/newsroom-references-view.tsx +141 -257
- package/components/notification-center.tsx +1 -1
- package/components/papyrus-console-frame.tsx +66 -0
- package/components/papyrus-console-shell.tsx +12 -41
- package/components/papyrus-private-chat-panel.tsx +50 -16
- package/components/proposals-panel.tsx +110 -0
- package/components/proposals-view.tsx +93 -0
- package/components/reference-review-card.tsx +383 -0
- package/components/topic-steering-workspace.tsx +13 -2
- package/components/use-console-visibility.ts +47 -0
- package/lib/cli-login-discovery.ts +78 -0
- package/lib/monitoring-client.ts +11 -2
- package/lib/monitoring-overview-fixture.ts +2 -2
- package/lib/monitoring-overview.ts +1 -2
- package/lib/newsroom-chat-client.ts +30 -0
- package/lib/newsroom-chat-identity.ts +39 -0
- package/lib/papyrus-hosted-chat.ts +62 -80
- package/lib/papyrus-private-chat-client.ts +7 -4
- package/lib/proposals-admin.ts +115 -0
- package/lib/prospectus-session.ts +2 -0
- package/lib/prospectus-sign-in-routes.ts +19 -2
- package/lib/prospectus-site-identity.ts +7 -0
- package/lib/relevance-decisions.ts +9 -0
- package/lib/review-requests.ts +4 -2
- package/package.json +12 -1
- package/routes.manifest.json +12 -1
- package/src/sync.mjs +11 -2
- package/src/with-papyrus.mjs +1 -1
- /package/amplify/{plugins/reader-steering/function → functions/proposal-actions}/proposals.py +0 -0
|
@@ -14,9 +14,8 @@ export const READER_POLL_SECRET_PARAMETER_ENV = "PAPYRUS_READER_POLL_SECRET_PARA
|
|
|
14
14
|
export const readerSteeringManifest = {
|
|
15
15
|
name: "reader-steering",
|
|
16
16
|
contract: 1,
|
|
17
|
-
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"
|
|
18
|
-
mutations: ["moderateReader"
|
|
19
|
-
queries: ["listStoryDrafts"],
|
|
17
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"],
|
|
18
|
+
mutations: ["moderateReader"],
|
|
20
19
|
guest: { mutations: [] },
|
|
21
20
|
newsroomPanels: [{ id: "moderation", label: "Moderation" }],
|
|
22
21
|
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
@@ -119,7 +118,6 @@ export const readerSteeringPlugin = {
|
|
|
119
118
|
["PAPYRUS_READER_COMMENT_TABLE", "ReaderComment"],
|
|
120
119
|
["PAPYRUS_READER_REPORT_TABLE", "ReaderReport"],
|
|
121
120
|
["PAPYRUS_READER_ACTION_TABLE", "ModerationAction"],
|
|
122
|
-
["PAPYRUS_READER_PROPOSAL_TABLE", "Proposal"],
|
|
123
121
|
]) {
|
|
124
122
|
tables[model].grantReadWriteData(lambda);
|
|
125
123
|
lambda.addEnvironment(variable, tables[model].tableName);
|
|
@@ -136,8 +134,10 @@ export const readerSteeringPlugin = {
|
|
|
136
134
|
// The snapshot reads submissions, and the review step writes their signals and moderation state.
|
|
137
135
|
tables.ReaderSubmission.grantReadWriteData(contentActions);
|
|
138
136
|
tables.ReaderComment.grantReadWriteData(contentActions);
|
|
139
|
-
// The snapshot reads proposals and freezes the community result of one that reaches editor review.
|
|
137
|
+
// The snapshot reads proposals and freezes the community result of one that reaches editor review. The Proposal model and the
|
|
138
|
+
// functions that file and move proposals are core (amplify/functions/proposal-actions); this plugin only reads the same table.
|
|
140
139
|
tables.Proposal.grantReadWriteData(contentActions);
|
|
140
|
+
contentActions.addEnvironment("PAPYRUS_READER_PROPOSAL_TABLE", tables.Proposal.tableName);
|
|
141
141
|
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
142
142
|
if (config.reservedConcurrency !== undefined) {
|
|
143
143
|
lambda.node.defaultChild.addPropertyOverride("ReservedConcurrentExecutions", config.reservedConcurrency);
|
|
@@ -21,9 +21,8 @@ export const READER_POLL_SECRET_PARAMETER_ENV = "PAPYRUS_READER_POLL_SECRET_PARA
|
|
|
21
21
|
export const readerSteeringManifest: PluginManifest = {
|
|
22
22
|
name: "reader-steering",
|
|
23
23
|
contract: 1,
|
|
24
|
-
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"
|
|
25
|
-
mutations: ["moderateReader"
|
|
26
|
-
queries: ["listStoryDrafts"],
|
|
24
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"],
|
|
25
|
+
mutations: ["moderateReader"],
|
|
27
26
|
guest: { mutations: [] },
|
|
28
27
|
newsroomPanels: [{ id: "moderation", label: "Moderation" }],
|
|
29
28
|
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
@@ -138,7 +137,6 @@ export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerA
|
|
|
138
137
|
["PAPYRUS_READER_COMMENT_TABLE", "ReaderComment"],
|
|
139
138
|
["PAPYRUS_READER_REPORT_TABLE", "ReaderReport"],
|
|
140
139
|
["PAPYRUS_READER_ACTION_TABLE", "ModerationAction"],
|
|
141
|
-
["PAPYRUS_READER_PROPOSAL_TABLE", "Proposal"],
|
|
142
140
|
] as const) {
|
|
143
141
|
tables[model].grantReadWriteData(lambda);
|
|
144
142
|
lambda.addEnvironment(variable, tables[model].tableName);
|
|
@@ -155,8 +153,10 @@ export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerA
|
|
|
155
153
|
// The snapshot reads submissions, and the review step writes their signals and moderation state.
|
|
156
154
|
tables.ReaderSubmission.grantReadWriteData(contentActions);
|
|
157
155
|
tables.ReaderComment.grantReadWriteData(contentActions);
|
|
158
|
-
// The snapshot reads proposals and freezes the community result of one that reaches editor review.
|
|
156
|
+
// The snapshot reads proposals and freezes the community result of one that reaches editor review. The Proposal model and the
|
|
157
|
+
// functions that file and move proposals are core (amplify/functions/proposal-actions); this plugin only reads the same table.
|
|
159
158
|
tables.Proposal.grantReadWriteData(contentActions);
|
|
159
|
+
contentActions.addEnvironment("PAPYRUS_READER_PROPOSAL_TABLE", tables.Proposal.tableName);
|
|
160
160
|
|
|
161
161
|
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
162
162
|
if (config.reservedConcurrency !== undefined) {
|
|
@@ -135,101 +135,6 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
135
135
|
.disableOperations(["subscriptions"])
|
|
136
136
|
.secondaryIndexes((index) => [index("target").sortKeys(["at"]).name("actionsByTarget").queryField("listModerationActionsByTargetAndAt")])
|
|
137
137
|
.authorization(staffOnly),
|
|
138
|
-
Proposal: a
|
|
139
|
-
.model({
|
|
140
|
-
id: a.id().required(),
|
|
141
|
-
kind: a.string().required(),
|
|
142
|
-
title: a.string().required(),
|
|
143
|
-
summary: a.string(),
|
|
144
|
-
body: a.string().required(),
|
|
145
|
-
targets: a.json().required(),
|
|
146
|
-
evidence: a.json(),
|
|
147
|
-
proposer: a.string().required(),
|
|
148
|
-
state: a.string().required(),
|
|
149
|
-
history: a.json().required(),
|
|
150
|
-
communityResult: a.json(),
|
|
151
|
-
pinned: a.boolean(),
|
|
152
|
-
held: a.boolean(),
|
|
153
|
-
override: a.json(),
|
|
154
|
-
exclusions: a.json(),
|
|
155
|
-
editorLog: a.json(),
|
|
156
|
-
needsLook: a.json(),
|
|
157
|
-
createdAt: a.datetime().required(),
|
|
158
|
-
})
|
|
159
|
-
.disableOperations(["subscriptions"])
|
|
160
|
-
.secondaryIndexes((index) => [index("state").sortKeys(["createdAt"]).name("proposalsByState").queryField("listProposalsByStateAndCreatedAt")])
|
|
161
|
-
.authorization(staffOnly),
|
|
162
|
-
ProposalReceipt: a.customType({
|
|
163
|
-
status: a.string().required(),
|
|
164
|
-
id: a.string(),
|
|
165
|
-
error: a.string(),
|
|
166
|
-
message: a.string(),
|
|
167
|
-
}),
|
|
168
|
-
// What an operator may read of a story draft: these six fields and nothing else (no body, evidence, history, proposer, vote or log).
|
|
169
|
-
StoryDraftSummary: a.customType({
|
|
170
|
-
id: a.string().required(),
|
|
171
|
-
title: a.string().required(),
|
|
172
|
-
summary: a.string(),
|
|
173
|
-
targets: a.json(),
|
|
174
|
-
state: a.string().required(),
|
|
175
|
-
createdAt: a.string().required(),
|
|
176
|
-
}),
|
|
177
|
-
StoryDraftList: a.customType({
|
|
178
|
-
status: a.string().required(),
|
|
179
|
-
id: a.string(),
|
|
180
|
-
error: a.string(),
|
|
181
|
-
message: a.string(),
|
|
182
|
-
drafts: a.ref("StoryDraftSummary").array(),
|
|
183
|
-
}),
|
|
184
|
-
// Editors and admins file as themselves. AppSync also admits an IAM principal whose own policy allows the field (the
|
|
185
|
-
// schema accepts IAM callers for the functions that call it); the function files an AI proposal for an IAM principal
|
|
186
|
-
// of its own account only, and refuses an identity-pool user and any other account. No IAM rule is declared here.
|
|
187
|
-
fileProposal: a
|
|
188
|
-
.mutation()
|
|
189
|
-
.arguments({
|
|
190
|
-
kind: a.string().required(),
|
|
191
|
-
title: a.string().required(),
|
|
192
|
-
body: a.string().required(),
|
|
193
|
-
summary: a.string(),
|
|
194
|
-
targets: a.json(),
|
|
195
|
-
evidence: a.json(),
|
|
196
|
-
})
|
|
197
|
-
.returns(a.ref("ProposalReceipt"))
|
|
198
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
199
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
200
|
-
// The same IAM rule as fileProposal, narrower: an operator may move only draft to in_steering or declined.
|
|
201
|
-
// Approving, deciding after review and overriding are refused for IAM in the function (`operator_not_allowed`).
|
|
202
|
-
transitionProposal: a
|
|
203
|
-
.mutation()
|
|
204
|
-
.arguments({
|
|
205
|
-
proposalId: a.string().required(),
|
|
206
|
-
state: a.string().required(),
|
|
207
|
-
reason: a.string(),
|
|
208
|
-
result: a.string(),
|
|
209
|
-
})
|
|
210
|
-
.returns(a.ref("ProposalReceipt"))
|
|
211
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
212
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
213
|
-
// Read only, and the same caller rule as fileProposal: editors and admins, or an IAM principal of the function's own account. The function
|
|
214
|
-
// answers the story drafts' id, title, summary, targets, state and createdAt so an unattended trend run can dedupe; nothing else.
|
|
215
|
-
listStoryDrafts: a
|
|
216
|
-
.query()
|
|
217
|
-
.returns(a.ref("StoryDraftList"))
|
|
218
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
219
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
220
|
-
// Editors and admins only, and the function refuses an IAM operator: pin, unpin, hold, release, exclude_votes and
|
|
221
|
-
// undo_exclusion. `rule` is a batch rule for exclude_votes; `exclusionId` names the exclusion undo_exclusion reverses.
|
|
222
|
-
editProposal: a
|
|
223
|
-
.mutation()
|
|
224
|
-
.arguments({
|
|
225
|
-
proposalId: a.string().required(),
|
|
226
|
-
action: a.string().required(),
|
|
227
|
-
rule: a.string(),
|
|
228
|
-
exclusionId: a.string(),
|
|
229
|
-
})
|
|
230
|
-
.returns(a.ref("ProposalReceipt"))
|
|
231
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
232
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
233
138
|
ModerationReceipt: a.customType({
|
|
234
139
|
status: a.string().required(),
|
|
235
140
|
id: a.string(),
|
|
@@ -137,101 +137,6 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
137
137
|
.disableOperations(["subscriptions"])
|
|
138
138
|
.secondaryIndexes((index) => [index("target").sortKeys(["at"]).name("actionsByTarget").queryField("listModerationActionsByTargetAndAt")])
|
|
139
139
|
.authorization(staffOnly),
|
|
140
|
-
Proposal: a
|
|
141
|
-
.model({
|
|
142
|
-
id: a.id().required(),
|
|
143
|
-
kind: a.string().required(),
|
|
144
|
-
title: a.string().required(),
|
|
145
|
-
summary: a.string(),
|
|
146
|
-
body: a.string().required(),
|
|
147
|
-
targets: a.json().required(),
|
|
148
|
-
evidence: a.json(),
|
|
149
|
-
proposer: a.string().required(),
|
|
150
|
-
state: a.string().required(),
|
|
151
|
-
history: a.json().required(),
|
|
152
|
-
communityResult: a.json(),
|
|
153
|
-
pinned: a.boolean(),
|
|
154
|
-
held: a.boolean(),
|
|
155
|
-
override: a.json(),
|
|
156
|
-
exclusions: a.json(),
|
|
157
|
-
editorLog: a.json(),
|
|
158
|
-
needsLook: a.json(),
|
|
159
|
-
createdAt: a.datetime().required(),
|
|
160
|
-
})
|
|
161
|
-
.disableOperations(["subscriptions"])
|
|
162
|
-
.secondaryIndexes((index) => [index("state").sortKeys(["createdAt"]).name("proposalsByState").queryField("listProposalsByStateAndCreatedAt")])
|
|
163
|
-
.authorization(staffOnly),
|
|
164
|
-
ProposalReceipt: a.customType({
|
|
165
|
-
status: a.string().required(),
|
|
166
|
-
id: a.string(),
|
|
167
|
-
error: a.string(),
|
|
168
|
-
message: a.string(),
|
|
169
|
-
}),
|
|
170
|
-
// What an operator may read of a story draft: these six fields and nothing else (no body, evidence, history, proposer, vote or log).
|
|
171
|
-
StoryDraftSummary: a.customType({
|
|
172
|
-
id: a.string().required(),
|
|
173
|
-
title: a.string().required(),
|
|
174
|
-
summary: a.string(),
|
|
175
|
-
targets: a.json(),
|
|
176
|
-
state: a.string().required(),
|
|
177
|
-
createdAt: a.string().required(),
|
|
178
|
-
}),
|
|
179
|
-
StoryDraftList: a.customType({
|
|
180
|
-
status: a.string().required(),
|
|
181
|
-
id: a.string(),
|
|
182
|
-
error: a.string(),
|
|
183
|
-
message: a.string(),
|
|
184
|
-
drafts: a.ref("StoryDraftSummary").array(),
|
|
185
|
-
}),
|
|
186
|
-
// Editors and admins file as themselves. AppSync also admits an IAM principal whose own policy allows the field (the
|
|
187
|
-
// schema accepts IAM callers for the functions that call it); the function files an AI proposal for an IAM principal
|
|
188
|
-
// of its own account only, and refuses an identity-pool user and any other account. No IAM rule is declared here.
|
|
189
|
-
fileProposal: a
|
|
190
|
-
.mutation()
|
|
191
|
-
.arguments({
|
|
192
|
-
kind: a.string().required(),
|
|
193
|
-
title: a.string().required(),
|
|
194
|
-
body: a.string().required(),
|
|
195
|
-
summary: a.string(),
|
|
196
|
-
targets: a.json(),
|
|
197
|
-
evidence: a.json(),
|
|
198
|
-
})
|
|
199
|
-
.returns(a.ref("ProposalReceipt"))
|
|
200
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
201
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
202
|
-
// The same IAM rule as fileProposal, narrower: an operator may move only draft to in_steering or declined.
|
|
203
|
-
// Approving, deciding after review and overriding are refused for IAM in the function (`operator_not_allowed`).
|
|
204
|
-
transitionProposal: a
|
|
205
|
-
.mutation()
|
|
206
|
-
.arguments({
|
|
207
|
-
proposalId: a.string().required(),
|
|
208
|
-
state: a.string().required(),
|
|
209
|
-
reason: a.string(),
|
|
210
|
-
result: a.string(),
|
|
211
|
-
})
|
|
212
|
-
.returns(a.ref("ProposalReceipt"))
|
|
213
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
214
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
215
|
-
// Read only, and the same caller rule as fileProposal: editors and admins, or an IAM principal of the function's own account. The function
|
|
216
|
-
// answers the story drafts' id, title, summary, targets, state and createdAt so an unattended trend run can dedupe; nothing else.
|
|
217
|
-
listStoryDrafts: a
|
|
218
|
-
.query()
|
|
219
|
-
.returns(a.ref("StoryDraftList"))
|
|
220
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
221
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
222
|
-
// Editors and admins only, and the function refuses an IAM operator: pin, unpin, hold, release, exclude_votes and
|
|
223
|
-
// undo_exclusion. `rule` is a batch rule for exclude_votes; `exclusionId` names the exclusion undo_exclusion reverses.
|
|
224
|
-
editProposal: a
|
|
225
|
-
.mutation()
|
|
226
|
-
.arguments({
|
|
227
|
-
proposalId: a.string().required(),
|
|
228
|
-
action: a.string().required(),
|
|
229
|
-
rule: a.string(),
|
|
230
|
-
exclusionId: a.string(),
|
|
231
|
-
})
|
|
232
|
-
.returns(a.ref("ProposalReceipt"))
|
|
233
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
234
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
235
140
|
ModerationReceipt: a.customType({
|
|
236
141
|
status: a.string().required(),
|
|
237
142
|
id: a.string(),
|
package/amplify/site-backend.js
CHANGED
|
@@ -24,6 +24,8 @@ import { defineSiteData } from "./data/resource.js";
|
|
|
24
24
|
import { editorialChecks } from "./functions/editorial-checks/resource.js";
|
|
25
25
|
import { automationView } from "./functions/automation-view/resource.js";
|
|
26
26
|
import { wireAutomationView } from "./functions/automation-view/wire.js";
|
|
27
|
+
import { proposalActions } from "./functions/proposal-actions/resource.js";
|
|
28
|
+
import { wireProposalActions } from "./functions/proposal-actions/wire.js";
|
|
27
29
|
import { monitoringView } from "./functions/monitoring-view/resource.js";
|
|
28
30
|
import { wireEmailReferenceIntake, wireMonitoringView } from "./functions/monitoring-view/wire.js";
|
|
29
31
|
import { wireEditorialChecks } from "./functions/editorial-checks/wire.js";
|
|
@@ -110,6 +112,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
110
112
|
modelAttachmentUpload,
|
|
111
113
|
newsroomSummary,
|
|
112
114
|
procedureAction,
|
|
115
|
+
proposalActions,
|
|
113
116
|
publicForms,
|
|
114
117
|
readerSettings,
|
|
115
118
|
emailSubmissionProcessor,
|
|
@@ -631,6 +634,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
631
634
|
wireEditorialChecks(backend);
|
|
632
635
|
wireMonitoringView(backend, emailMonitorId);
|
|
633
636
|
wireAutomationView(backend);
|
|
637
|
+
wireProposalActions(backend);
|
|
634
638
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
635
639
|
return backend;
|
|
636
640
|
}
|
package/amplify/site-backend.ts
CHANGED
|
@@ -31,6 +31,8 @@ import { defineSiteData } from "./data/resource";
|
|
|
31
31
|
import { editorialChecks } from "./functions/editorial-checks/resource";
|
|
32
32
|
import { automationView } from "./functions/automation-view/resource";
|
|
33
33
|
import { wireAutomationView } from "./functions/automation-view/wire";
|
|
34
|
+
import { proposalActions } from "./functions/proposal-actions/resource";
|
|
35
|
+
import { wireProposalActions } from "./functions/proposal-actions/wire";
|
|
34
36
|
import { monitoringView } from "./functions/monitoring-view/resource";
|
|
35
37
|
import { wireEmailReferenceIntake, wireMonitoringView } from "./functions/monitoring-view/wire";
|
|
36
38
|
import { wireEditorialChecks } from "./functions/editorial-checks/wire";
|
|
@@ -131,6 +133,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
131
133
|
modelAttachmentUpload,
|
|
132
134
|
newsroomSummary,
|
|
133
135
|
procedureAction,
|
|
136
|
+
proposalActions,
|
|
134
137
|
publicForms,
|
|
135
138
|
readerSettings,
|
|
136
139
|
emailSubmissionProcessor,
|
|
@@ -777,6 +780,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
777
780
|
wireEditorialChecks(backend);
|
|
778
781
|
wireMonitoringView(backend, emailMonitorId);
|
|
779
782
|
wireAutomationView(backend);
|
|
783
|
+
wireProposalActions(backend);
|
|
780
784
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
781
785
|
|
|
782
786
|
return backend;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleCliLoginDiscovery } from "../../../lib/cli-login-discovery";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleCliLoginDiscovery(request);
|
|
9
|
+
}
|
package/app/globals.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@import "./papyrus-theme.css";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/* Tailwind (import, @source globs, @theme tokens) lives only in ./tailwind.css. Importing it again here
|
|
4
|
+
emitted a second full Tailwind build that loaded later and whose `.hidden` overrode `md:flex`. */
|
|
5
5
|
@import "@radix-ui/colors/slate.css";
|
|
6
6
|
@import "@radix-ui/colors/slate-alpha.css";
|
|
7
7
|
@import "@radix-ui/colors/slate-dark.css";
|
|
@@ -15,39 +15,6 @@
|
|
|
15
15
|
@import "@radix-ui/colors/tomato-dark.css";
|
|
16
16
|
@import "@radix-ui/colors/tomato-dark-alpha.css";
|
|
17
17
|
|
|
18
|
-
@theme inline {
|
|
19
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
20
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
21
|
-
--radius-lg: var(--radius);
|
|
22
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
23
|
-
--color-background: var(--background);
|
|
24
|
-
--color-foreground: var(--foreground);
|
|
25
|
-
--color-card: var(--card);
|
|
26
|
-
--color-card-foreground: var(--card-foreground);
|
|
27
|
-
--color-popover: var(--popover);
|
|
28
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
29
|
-
--color-primary: var(--primary);
|
|
30
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
31
|
-
--color-secondary: var(--secondary);
|
|
32
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
-
--color-muted: var(--muted);
|
|
34
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
35
|
-
--color-accent: var(--accent);
|
|
36
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
37
|
-
--color-destructive: var(--destructive);
|
|
38
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
39
|
-
--color-border: var(--border);
|
|
40
|
-
--color-input: var(--input);
|
|
41
|
-
--color-ring: var(--ring);
|
|
42
|
-
--color-danger: var(--danger);
|
|
43
|
-
--color-danger-foreground: var(--danger-foreground);
|
|
44
|
-
--color-surface: var(--surface);
|
|
45
|
-
--color-surface-foreground: var(--surface-foreground);
|
|
46
|
-
--color-foreground-strong: var(--foreground-strong);
|
|
47
|
-
--color-foreground-muted: var(--foreground-muted);
|
|
48
|
-
--color-foreground-subtle: var(--foreground-subtle);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
18
|
:root {
|
|
52
19
|
--radius: 0.875rem;
|
|
53
20
|
--background: var(--slate-2);
|
|
@@ -10170,3 +10137,32 @@ img {
|
|
|
10170
10137
|
}
|
|
10171
10138
|
}
|
|
10172
10139
|
|
|
10140
|
+
|
|
10141
|
+
@media (max-width: 900px) {
|
|
10142
|
+
.edition-progress__button--console { display: none; }
|
|
10143
|
+
.papyrus-console--open {
|
|
10144
|
+
--papyrus-console-width: 100vw;
|
|
10145
|
+
background: rgb(var(--paper-rgb));
|
|
10146
|
+
height: 100dvh;
|
|
10147
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
10148
|
+
box-sizing: border-box;
|
|
10149
|
+
}
|
|
10150
|
+
}
|
|
10151
|
+
|
|
10152
|
+
.papyrus-console__access {
|
|
10153
|
+
display: flex;
|
|
10154
|
+
flex-direction: column;
|
|
10155
|
+
align-items: flex-start;
|
|
10156
|
+
gap: 16px;
|
|
10157
|
+
padding: 24px 18px;
|
|
10158
|
+
overflow-y: auto;
|
|
10159
|
+
color: var(--ink);
|
|
10160
|
+
font-size: 0.95rem;
|
|
10161
|
+
line-height: 1.5;
|
|
10162
|
+
}
|
|
10163
|
+
.papyrus-console__access strong { font-size: 1.15rem; }
|
|
10164
|
+
.papyrus-console__access button { min-height: 44px; padding: 10px 14px; font: inherit; text-transform: none; letter-spacing: normal; }
|
|
10165
|
+
|
|
10166
|
+
@media (max-width: 900px) {
|
|
10167
|
+
.papyrus-console__header button { min-width: 44px; min-height: 44px; }
|
|
10168
|
+
}
|
package/app/tailwind.css
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
--color-accent: var(--accent);
|
|
25
25
|
--color-accent-foreground: var(--accent-foreground);
|
|
26
26
|
--color-destructive: var(--destructive);
|
|
27
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
28
|
+
--color-foreground-subtle: var(--foreground-subtle);
|
|
27
29
|
--color-danger: var(--danger);
|
|
28
30
|
--color-danger-foreground: var(--danger-foreground);
|
|
29
31
|
--color-surface: var(--surface);
|
|
@@ -97,8 +97,8 @@ function QueueItem({ item, ownId, demo }: { item: MonitoringItem; ownId: string
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImportCandidates }: {
|
|
101
|
-
items: MonitoringEmailSummary[]; detail: MonitoringEmailDetails | null; onOpen
|
|
100
|
+
export function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImportCandidates }: {
|
|
101
|
+
items: MonitoringEmailSummary[]; detail: MonitoringEmailDetails | null; onOpen?: (receiptId: string) => void;
|
|
102
102
|
onResolveArticleLink?: (receiptId: string, candidateIndex: number) => Promise<MonitoringEmailArticleResolution>;
|
|
103
103
|
onImportCandidates?: (receiptId: string, items: EmailReferenceImportItem[]) => Promise<EmailReferenceImportOutcome>;
|
|
104
104
|
}) {
|
|
@@ -138,7 +138,10 @@ function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImpor
|
|
|
138
138
|
const result = await onResolveArticleLink(requestReceiptId, index);
|
|
139
139
|
if (!isCurrentEmailSubmissionAction(activeReceiptId.current, requestReceiptId)) return;
|
|
140
140
|
if (!("url" in result)) {
|
|
141
|
-
|
|
141
|
+
const reason = "reason" in result && typeof result.reason === "string"
|
|
142
|
+
&& /^(?:redirect_[a-z_]+|resolution_record_invalid|resolution_not_recorded)$/.test(result.reason)
|
|
143
|
+
? ` (${result.reason})` : "";
|
|
144
|
+
setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: `The article destination could not be resolved safely${reason}. The original link remains private and unchanged.` }));
|
|
142
145
|
return;
|
|
143
146
|
}
|
|
144
147
|
const candidate = detail && "submission" in detail ? detail.candidateLinks[index] : null;
|
|
@@ -181,7 +184,7 @@ function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImpor
|
|
|
181
184
|
if (result.error) {
|
|
182
185
|
setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: `${result.imported} of ${candidates.length} selected candidates were registered or matched. ${result.error}` }));
|
|
183
186
|
} else {
|
|
184
|
-
setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: `${result.imported} selected candidates are available as References. Import does not approve or publish them.` }));
|
|
187
|
+
setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: `${result.imported} selected candidates are available as References. Predictions are queued in the cloud; review them when ready. Import does not approve or publish them.` }));
|
|
185
188
|
}
|
|
186
189
|
} catch {
|
|
187
190
|
if (isCurrentEmailSubmissionAction(activeReceiptId.current, requestReceiptId)) {
|
|
@@ -207,7 +210,7 @@ function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImpor
|
|
|
207
210
|
<span>SES signals — SPF {item.verdicts.spf}, DKIM {item.verdicts.dkim}, DMARC {item.verdicts.dmarc}; spam {item.verdicts.spam}, virus {item.verdicts.virus}</span>
|
|
208
211
|
{item.warnings.length ? <span>Warnings: {item.warnings.join(", ")}</span> : null}
|
|
209
212
|
{item.errorCode ? <span>State: {item.status} ({item.errorCode})</span> : <span>State: {item.status}</span>}
|
|
210
|
-
<button type="button" onClick={() => onOpen(item.receiptId)}>Read email and review candidate links</button>
|
|
213
|
+
{onOpen ? <button type="button" onClick={() => onOpen(item.receiptId)}>Read email and review candidate links</button> : null}
|
|
211
214
|
</li>)}
|
|
212
215
|
</ul> : <p>No email submissions are available for this publication.</p>}
|
|
213
216
|
{detail && "submission" in detail ? (
|
|
@@ -238,7 +241,9 @@ function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImpor
|
|
|
238
241
|
{busyImport ? "Adding References…" : "Add selected links to References"}
|
|
239
242
|
</button>
|
|
240
243
|
{notice ? <p role="status" data-email-import-result>{notice}</p> : null}
|
|
241
|
-
{importedReferences.length ? <
|
|
244
|
+
{importedReferences.length ? <ul>{importedReferences.map((reference, index) => (
|
|
245
|
+
<li key={`${reference.id}-${index}`}><Link href={getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(reference.lineageId)}`, false)}>Review imported Reference {index + 1}</Link></li>
|
|
246
|
+
))}</ul> : null}
|
|
242
247
|
</section>
|
|
243
248
|
) : detail && "error" in detail ? <p role="alert">You cannot read this submission.</p>
|
|
244
249
|
: detail && "state" in detail ? <p role="alert">Submission details are unavailable.</p> : null}
|
|
@@ -297,7 +302,7 @@ function Ready({ data, demo, emailDetail, onEmailOpen, onResolveArticleLink, onI
|
|
|
297
302
|
<strong>{feed.id}</strong>
|
|
298
303
|
<span>{feedHost(feed.sourceUrl)}</span>
|
|
299
304
|
</div>
|
|
300
|
-
<span>{feed.enabled ? "Enabled" : "Disabled"} · {everyText(feed.intervalSeconds)}
|
|
305
|
+
<span>{feed.enabled ? "Enabled" : "Disabled"} · {everyText(feed.intervalSeconds)}</span>
|
|
301
306
|
<span>Last run {formatUtc(feed.lastFinishedAt)}{feed.lastStatus ? ` (${status(feed.lastStatus)})` : ""} · next {formatUtc(feed.nextDueAt)}</span>
|
|
302
307
|
</li>
|
|
303
308
|
))}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
+
import { useSearchParams } from "next/navigation";
|
|
4
5
|
import {
|
|
5
6
|
importMonitoringEmailCandidates,
|
|
6
7
|
loadMonitoringEmailSubmission,
|
|
@@ -9,12 +10,41 @@ import {
|
|
|
9
10
|
} from "../lib/monitoring-client";
|
|
10
11
|
import { FIXTURE_ALL_PIPELINE_OVERVIEW, FIXTURE_MONITORING_OVERVIEW } from "../lib/monitoring-overview-fixture";
|
|
11
12
|
import type { EmailReferenceImportItem, MonitoringEmailDetails, MonitoringResult } from "../lib/monitoring-overview";
|
|
12
|
-
import { MonitoringOverviewView, type MonitoringMode } from "./monitoring-overview-view";
|
|
13
|
+
import { EmailSubmissions, MonitoringOverviewView, type MonitoringMode } from "./monitoring-overview-view";
|
|
13
14
|
|
|
14
15
|
const REFRESH_MILLISECONDS = 60_000;
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
export function MonitoringPanel(
|
|
17
|
+
/** A receipt URL mounts only the single-submission reader, never the overview. */
|
|
18
|
+
export function MonitoringPanel(props: { isAdmin: boolean; isDemo: boolean }) {
|
|
19
|
+
const searchParams = useSearchParams();
|
|
20
|
+
if (searchParams.has("receipt")) {
|
|
21
|
+
return <MonitoringEmailReceipt key={searchParams.get("receipt")} receiptId={searchParams.get("receipt") ?? ""} isDemo={props.isDemo} />;
|
|
22
|
+
}
|
|
23
|
+
return <MonitoringOverviewPanel {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function MonitoringEmailReceipt({ receiptId, isDemo }: { receiptId: string; isDemo: boolean }) {
|
|
27
|
+
const [detail, setDetail] = useState<MonitoringEmailDetails | null>(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!receiptId || isDemo) return;
|
|
30
|
+
let live = true;
|
|
31
|
+
void loadMonitoringEmailSubmission(receiptId).then(
|
|
32
|
+
(next) => { if (live) setDetail(next); },
|
|
33
|
+
() => { if (live) setDetail({ state: "unavailable" }); },
|
|
34
|
+
);
|
|
35
|
+
return () => { live = false; };
|
|
36
|
+
}, [receiptId, isDemo]);
|
|
37
|
+
if (!receiptId) return <p role="alert">A receipt is required to open a submission.</p>;
|
|
38
|
+
if (isDemo) return <p role="status">Single email submissions require an editor session.</p>;
|
|
39
|
+
return <div data-news-desk-admin-panel="monitoring">
|
|
40
|
+
{detail === null ? <p role="status">Loading email submission…</p> :
|
|
41
|
+
<EmailSubmissions items={"submission" in detail ? [detail.submission] : []} detail={detail}
|
|
42
|
+
onResolveArticleLink={resolveMonitoringEmailArticleLink} onImportCandidates={importMonitoringEmailCandidates} />}
|
|
43
|
+
</div>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Overview refreshes apply only when no receipt was requested. */
|
|
47
|
+
function MonitoringOverviewPanel({ isAdmin, isDemo }: { isAdmin: boolean; isDemo: boolean }) {
|
|
18
48
|
const [mode, setMode] = useState<MonitoringMode>("publication");
|
|
19
49
|
const [result, setResult] = useState<MonitoringResult | null>(null);
|
|
20
50
|
const [emailDetail, setEmailDetail] = useState<MonitoringEmailDetails | null>(null);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { BookOpen, ClipboardList, House, MessageCircle, MessagesSquare } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
const ICONS = { overview: House, articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
|
|
8
|
+
|
|
9
|
+
export function NewsroomMobileNavigation({ items, activeTab, chat, more }: {
|
|
10
|
+
items: readonly { key: string; label: string; description: string; href: string }[];
|
|
11
|
+
activeTab: string;
|
|
12
|
+
chat?: { open: boolean; onOpen: () => void };
|
|
13
|
+
more: ReactNode;
|
|
14
|
+
}) {
|
|
15
|
+
return <nav className="organizer-bottom-nav" aria-label="Primary newsroom navigation"><ul>
|
|
16
|
+
{items.map(item => {
|
|
17
|
+
if (item.key === "messages" && chat) return <li key="chat"><button className="organizer-chat" type="button" aria-label="Open private chat" aria-expanded={chat.open} aria-controls="papyrus-console-panel" onClick={chat.onOpen}><MessageCircle size={18} aria-hidden="true" /><span>Chat</span></button></li>;
|
|
18
|
+
const Icon = ICONS[item.key as keyof typeof ICONS];
|
|
19
|
+
return <li key={item.key}><Link href={item.href} title={item.description} aria-label={item.label} aria-current={activeTab === item.key ? "page" : undefined}><Icon size={18} aria-hidden="true" /><span>{item.label}</span></Link></li>;
|
|
20
|
+
})}
|
|
21
|
+
<li>{more}</li>
|
|
22
|
+
</ul></nav>;
|
|
23
|
+
}
|
|
@@ -8,7 +8,7 @@ import { cn } from "../lib/utils";
|
|
|
8
8
|
import { useEffect, useId, useState, type ReactNode } from "react";
|
|
9
9
|
import Link from "next/link";
|
|
10
10
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
11
|
-
import {
|
|
11
|
+
import { Menu, Search, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
|
|
12
12
|
import { ORGANIZER_NAV_GROUPS, ORGANIZER_PRIMARY_NAV, ORGANIZER_SETTINGS_DESTINATION } from "../lib/newsroom-organizer-nav";
|
|
13
13
|
import { browserPathToInternalNewsroomPath } from "../lib/newsroom-base-path";
|
|
14
14
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
@@ -20,6 +20,8 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetTr
|
|
|
20
20
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator } from "./ui/dropdown-menu";
|
|
21
21
|
import "./newsroom-organizer.css";
|
|
22
22
|
import { NewsroomThemeFonts, useNewsroomThemeAttributes } from "./newsroom-theme";
|
|
23
|
+
import { NewsroomMobileNavigation } from "./newsroom-mobile-navigation";
|
|
24
|
+
import { usePapyrusConsole } from "./papyrus-console-shell";
|
|
23
25
|
|
|
24
26
|
export type NewsroomNavCount = {
|
|
25
27
|
count: number | null;
|
|
@@ -55,8 +57,6 @@ function OrganizerSearch({ demo }: { demo: boolean }) {
|
|
|
55
57
|
</form>;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
const ICONS = { overview: House, articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
|
|
59
|
-
|
|
60
60
|
/** Navigation is a presentation layer; existing route and server authorization still own access. */
|
|
61
61
|
function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {
|
|
62
62
|
const id = useId();
|
|
@@ -105,6 +105,7 @@ function Account({ placement }: { placement: "sidebar" | "header" }) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabel, children, demo = false, headerActions, pageTitle, showNavigation = true }: NewsroomOpsShellProps) {
|
|
108
|
+
const consoleState = usePapyrusConsole();
|
|
108
109
|
const pathname = usePathname();
|
|
109
110
|
const themeAttributes = useNewsroomThemeAttributes();
|
|
110
111
|
const session = useOptionalNewsDeskClient();
|
|
@@ -166,16 +167,13 @@ export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabe
|
|
|
166
167
|
</header>
|
|
167
168
|
<main className="organizer-main">{demo && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
|
|
168
169
|
</div>
|
|
169
|
-
{showNavigation && <
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
<Icon size={18} aria-hidden="true" /><span>{item.label}</span></Link></li>; })}
|
|
173
|
-
<li><Sheet open={menuOpen} onOpenChange={setMenuOpen}><SheetTrigger className="organizer-more" aria-label="More"><Menu size={18} aria-hidden="true" /><span>More</span></SheetTrigger>
|
|
170
|
+
{showNavigation && <NewsroomMobileNavigation activeTab={activeTab} items={ORGANIZER_PRIMARY_NAV.map(item => ({ ...item, href: getNewsroomNavHref(item.path, demo) }))}
|
|
171
|
+
chat={consoleState?.shouldOfferConsole ? { open: consoleState.open, onOpen: consoleState.openConsole } : undefined}
|
|
172
|
+
more={<Sheet open={menuOpen} onOpenChange={setMenuOpen}><SheetTrigger className="organizer-more" aria-label="More"><Menu size={18} aria-hidden="true" /><span>More</span></SheetTrigger>
|
|
174
173
|
<SheetContent side="bottom" className="organizer-menu" {...themeAttributes}><SheetHeader><SheetTitle>Newsroom</SheetTitle><SheetDescription>Workspaces and administration</SheetDescription></SheetHeader>
|
|
175
174
|
<Navigation activePath={activePath} demo={demo} admin={admin} surface="menu" onNavigate={() => setMenuOpen(false)} />
|
|
176
175
|
</SheetContent>
|
|
177
|
-
</Sheet
|
|
178
|
-
</ul></nav>}
|
|
176
|
+
</Sheet>} />}
|
|
179
177
|
</div>;
|
|
180
178
|
}
|
|
181
179
|
|
|
@@ -33,6 +33,7 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
33
33
|
.organizer-account-popup{min-width:250px;max-width:calc(100vw - 32px);border:1px solid var(--org-line);padding:6px;border-radius:4px}
|
|
34
34
|
.organizer-account-popup [role=menuitem]{min-height:40px;font-size:12px}.organizer-account-popup .reader-auth{display:contents}.organizer-account-popup .reader-auth__error{display:block;white-space:normal;font-size:11px;padding:8px}
|
|
35
35
|
.organizer-bottom-nav{display:none}
|
|
36
|
+
.organizer-bottom-nav button{appearance:none;border:0;background:transparent;color:inherit}
|
|
36
37
|
.organizer-main:has(.organizer-native-board){display:flex;overflow:hidden}
|
|
37
38
|
.organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:8px}
|
|
38
39
|
.organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
|