Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1750-project-board-scrim.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Project board now opens as a real modal: a dimming scrim sits in front of the board and covers the shell, so the bot tree and workflows no longer read as extra board rows, and a click beside the board closes it ([#1750](https://github.com/ThinkFlowLab/FlyBot/issues/1750)).
51 changes: 36 additions & 15 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19624,6 +19624,12 @@ function watchShellLevel(doc) {
// src/client/project-board.js
var import_react46 = require("react");
var PROJECT_BOARD_CSS = `
/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the
bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the
Board and Settings layers, with kanban's own 'bt-scrim' sibling and 'bt-mem' surface. */
.bt-project-overlay{position:fixed;inset:0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
.bt-mem.bt-project-dialog{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))}
.bt-project-body{flex:1 1 auto;min-height:0;overflow:auto}
.bt-project-board{padding:16px 18px}
.bt-project-group{margin:0 0 14px}
.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7}
Expand Down Expand Up @@ -19780,21 +19786,36 @@ function ProjectBoardOverlay({ actions } = {}) {
if (state === null) return null;
return (0, import_react46.createElement)(
"div",
{ className: "bt-overlay bt-project-overlay", role: "dialog", "aria-label": t("Project board") },
(0, import_react46.createElement)("button", { type: "button", className: "bt-project-close", onClick: close }, t("Close")),
state.project === null ? (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t("No Project here.")) : (0, import_react46.createElement)(ProjectBoardPane, {
project: state.project,
rows: state.rows,
canManage: state.canManage,
tasks: state.tasks,
// The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
// lands on that Bot or group. Nothing here re-decides which board that is.
onOpen: (board) => {
if (typeof window !== "undefined") window.location.hash = `#task=${board.kind}:${board.id}`;
actions?.openBoard?.();
setState(null);
}
})
{ className: "bt-project-overlay" },
(0, import_react46.createElement)("div", { className: "bt-scrim", onClick: close }),
(0, import_react46.createElement)(
"div",
{ className: "bt-mem bt-project-dialog", role: "dialog", "aria-modal": true, "aria-label": t("Project board") },
(0, import_react46.createElement)(
"div",
{ className: "bt-mem-head" },
(0, import_react46.createElement)("h2", {}, t("Project board")),
(0, import_react46.createElement)("span", { className: "bt-mem-head-gap" }),
(0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close"))
),
(0, import_react46.createElement)(
"div",
{ className: "bt-project-body" },
state.project === null ? (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t("No Project here.")) : (0, import_react46.createElement)(ProjectBoardPane, {
project: state.project,
rows: state.rows,
canManage: state.canManage,
tasks: state.tasks,
// The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
// lands on that Bot or group. Nothing here re-decides which board that is.
onOpen: (board) => {
if (typeof window !== "undefined") window.location.hash = `#task=${board.kind}:${board.id}`;
actions?.openBoard?.();
setState(null);
}
})
)
)
);
}

Expand Down
46 changes: 30 additions & 16 deletions core/src/client/project-board.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { call } from './api.js'
import { t } from './i18n.js'

export const PROJECT_BOARD_CSS = `
/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the
bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the
Board and Settings layers, with kanban's own 'bt-scrim' sibling and 'bt-mem' surface. */
.bt-project-overlay{position:fixed;inset:0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
.bt-mem.bt-project-dialog{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))}
.bt-project-body{flex:1 1 auto;min-height:0;overflow:auto}
.bt-project-board{padding:16px 18px}
.bt-project-group{margin:0 0 14px}
.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7}
Expand Down Expand Up @@ -164,20 +170,28 @@ export function ProjectBoardOverlay({ actions } = {}) {
return () => { live = false }
}, [hash])
if (state === null) return null
return h('div', { className: 'bt-overlay bt-project-overlay', role: 'dialog', 'aria-label': t('Project board') },
h('button', { type: 'button', className: 'bt-project-close', onClick: close }, t('Close')),
state.project === null
? h('div', { className: 'bt-project-empty' }, t('No Project here.'))
// A row opens the board its task lives on, carried by `#task=<id>`: the Tasks board reads
// that hash and lands on the Bot or group the task belongs to.
: h(ProjectBoardPane, {
project: state.project, rows: state.rows, canManage: state.canManage, tasks: state.tasks,
// The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
// lands on that Bot or group. Nothing here re-decides which board that is.
onOpen: (board) => {
if (typeof window !== 'undefined') window.location.hash = `#task=${board.kind}:${board.id}`
actions?.openBoard?.()
setState(null)
},
}))
// #1750: the scrim is a sibling in front of the dialog, exactly as the Board draws it, so a click
// beside the board dismisses it and the shell behind reads as a backdrop rather than more rows.
return h('div', { className: 'bt-project-overlay' },
h('div', { className: 'bt-scrim', onClick: close }),
h('div', { className: 'bt-mem bt-project-dialog', role: 'dialog', 'aria-modal': true, 'aria-label': t('Project board') },
h('div', { className: 'bt-mem-head' },
h('h2', {}, t('Project board')),
h('span', { className: 'bt-mem-head-gap' }),
h('button', { type: 'button', className: 'bt-soft bt-soft-sm bt-project-close', autoFocus: true, onClick: close }, t('Close'))),
h('div', { className: 'bt-project-body' },
state.project === null
? h('div', { className: 'bt-project-empty' }, t('No Project here.'))
// A row opens the board its task lives on, carried by `#task=<id>`: the Tasks board reads
// that hash and lands on the Bot or group the task belongs to.
: h(ProjectBoardPane, {
project: state.project, rows: state.rows, canManage: state.canManage, tasks: state.tasks,
// The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
// lands on that Bot or group. Nothing here re-decides which board that is.
onOpen: (board) => {
if (typeof window !== 'undefined') window.location.hash = `#task=${board.kind}:${board.id}`
actions?.openBoard?.()
setState(null)
},
}))))
}
24 changes: 24 additions & 0 deletions core/test/ui-clickthrough.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,27 @@ test('#1347: the built bundle carries these rules, so a page reload shows them',
assert.ok(bundle.includes('bt-chat-strip-name'), 'client.js carries the blank-chat strip')
assert.ok(bundle.includes('Conversation copied as Markdown'), 'client.js carries the copy confirmation')
})

test('#1750: the Project board overlay covers the shell behind its scrim', () => {
const css = read('project-board.js')
// The overlay carried `bt-overlay`, a class no stylesheet defines: the board rendered as a short
// dialog in the page flow, with the bot tree and workflows legible under its rows and nothing
// dimming them. A modal covers the whole viewport, so the shell cannot be read as more board rows.
const overlay = rule(css, '.bt-project-overlay')
assert.match(overlay, /position:fixed/, 'the layer is taken out of the page flow')
assert.match(overlay, /inset:0/, 'and covers the viewport, not just its own content height')
assert.match(overlay, /align-items:center/, 'the dialog sits centred in that layer')
assert.match(overlay, /justify-content:center/)
// Every other modal in the client pairs a `bt-scrim` sibling with its dialog and dismisses on a
// click; the Project board was the one overlay with no scrim at all.
assert.match(css, /className: 'bt-scrim', onClick: close/, 'the scrim renders beside the dialog and closes the overlay')
// Reuse, not a second visual language: the dialog is kanban's own `bt-mem` surface at kanban's size.
const dialog = rule(css, '.bt-mem.bt-project-dialog')
assert.match(dialog, /width:min\(1080px,calc\(100vw - 32px\)\)/, 'the same dialog width as the Board')
assert.match(dialog, /height:min\(760px,calc\(100vh - 48px\)\)/, 'and the same height, so board rows are not mixed with the shell')

// The app runs client.js, so the rule only reaches the page once it is in the bundle.
const bundle = readFileSync(new URL('../client.js', import.meta.url), 'utf8')
assert.match(rule(bundle, '.bt-project-overlay'), /position:fixed;inset:0/, 'client.js carries the covering layer')
assert.ok(bundle.includes('bt-mem bt-project-dialog'), 'and the dialog it centres')
})
53 changes: 37 additions & 16 deletions patches/ds-bot/0008-client-bundle.patch
Original file line number Diff line number Diff line change
Expand Up @@ -20056,7 +20056,7 @@ diff --git a/client.js b/client.js
}
function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) {
const views = /* @__PURE__ */ new Map();
@@ -13052,11 +19530,295 @@
@@ -13052,11 +19530,316 @@
};
}

Expand Down Expand Up @@ -20154,6 +20154,12 @@ diff --git a/client.js b/client.js
+// src/client/project-board.js
+var import_react46 = require("react");
+var PROJECT_BOARD_CSS = `
+/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the
+ bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the
+ Board and Settings layers, with kanban's own 'bt-scrim' sibling and 'bt-mem' surface. */
+.bt-project-overlay{position:fixed;inset:0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
+.bt-mem.bt-project-dialog{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))}
+.bt-project-body{flex:1 1 auto;min-height:0;overflow:auto}
+.bt-project-board{padding:16px 18px}
+.bt-project-group{margin:0 0 14px}
+.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7}
Expand Down Expand Up @@ -20310,21 +20316,36 @@ diff --git a/client.js b/client.js
+ if (state === null) return null;
+ return (0, import_react46.createElement)(
+ "div",
+ { className: "bt-overlay bt-project-overlay", role: "dialog", "aria-label": t("Project board") },
+ (0, import_react46.createElement)("button", { type: "button", className: "bt-project-close", onClick: close }, t("Close")),
+ state.project === null ? (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t("No Project here.")) : (0, import_react46.createElement)(ProjectBoardPane, {
+ project: state.project,
+ rows: state.rows,
+ canManage: state.canManage,
+ tasks: state.tasks,
+ // The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
+ // lands on that Bot or group. Nothing here re-decides which board that is.
+ onOpen: (board) => {
+ if (typeof window !== "undefined") window.location.hash = `#task=${board.kind}:${board.id}`;
+ actions?.openBoard?.();
+ setState(null);
+ }
+ })
+ { className: "bt-project-overlay" },
+ (0, import_react46.createElement)("div", { className: "bt-scrim", onClick: close }),
+ (0, import_react46.createElement)(
+ "div",
+ { className: "bt-mem bt-project-dialog", role: "dialog", "aria-modal": true, "aria-label": t("Project board") },
+ (0, import_react46.createElement)(
+ "div",
+ { className: "bt-mem-head" },
+ (0, import_react46.createElement)("h2", {}, t("Project board")),
+ (0, import_react46.createElement)("span", { className: "bt-mem-head-gap" }),
+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close"))
+ ),
+ (0, import_react46.createElement)(
+ "div",
+ { className: "bt-project-body" },
+ state.project === null ? (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t("No Project here.")) : (0, import_react46.createElement)(ProjectBoardPane, {
+ project: state.project,
+ rows: state.rows,
+ canManage: state.canManage,
+ tasks: state.tasks,
+ // The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
+ // lands on that Bot or group. Nothing here re-decides which board that is.
+ onOpen: (board) => {
+ if (typeof window !== "undefined") window.location.hash = `#task=${board.kind}:${board.id}`;
+ actions?.openBoard?.();
+ setState(null);
+ }
+ })
+ )
+ )
+ );
+}
+
Expand Down
46 changes: 30 additions & 16 deletions patches/ds-bot/files/src/client/project-board.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { call } from './api.js'
import { t } from './i18n.js'

export const PROJECT_BOARD_CSS = `
/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the
bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the
Board and Settings layers, with kanban's own 'bt-scrim' sibling and 'bt-mem' surface. */
.bt-project-overlay{position:fixed;inset:0;z-index:121;isolation:isolate;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
.bt-mem.bt-project-dialog{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))}
.bt-project-body{flex:1 1 auto;min-height:0;overflow:auto}
.bt-project-board{padding:16px 18px}
.bt-project-group{margin:0 0 14px}
.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7}
Expand Down Expand Up @@ -164,20 +170,28 @@ export function ProjectBoardOverlay({ actions } = {}) {
return () => { live = false }
}, [hash])
if (state === null) return null
return h('div', { className: 'bt-overlay bt-project-overlay', role: 'dialog', 'aria-label': t('Project board') },
h('button', { type: 'button', className: 'bt-project-close', onClick: close }, t('Close')),
state.project === null
? h('div', { className: 'bt-project-empty' }, t('No Project here.'))
// A row opens the board its task lives on, carried by `#task=<id>`: the Tasks board reads
// that hash and lands on the Bot or group the task belongs to.
: h(ProjectBoardPane, {
project: state.project, rows: state.rows, canManage: state.canManage, tasks: state.tasks,
// The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
// lands on that Bot or group. Nothing here re-decides which board that is.
onOpen: (board) => {
if (typeof window !== 'undefined') window.location.hash = `#task=${board.kind}:${board.id}`
actions?.openBoard?.()
setState(null)
},
}))
// #1750: the scrim is a sibling in front of the dialog, exactly as the Board draws it, so a click
// beside the board dismisses it and the shell behind reads as a backdrop rather than more rows.
return h('div', { className: 'bt-project-overlay' },
h('div', { className: 'bt-scrim', onClick: close }),
h('div', { className: 'bt-mem bt-project-dialog', role: 'dialog', 'aria-modal': true, 'aria-label': t('Project board') },
h('div', { className: 'bt-mem-head' },
h('h2', {}, t('Project board')),
h('span', { className: 'bt-mem-head-gap' }),
h('button', { type: 'button', className: 'bt-soft bt-soft-sm bt-project-close', autoFocus: true, onClick: close }, t('Close'))),
h('div', { className: 'bt-project-body' },
state.project === null
? h('div', { className: 'bt-project-empty' }, t('No Project here.'))
// A row opens the board its task lives on, carried by `#task=<id>`: the Tasks board reads
// that hash and lands on the Bot or group the task belongs to.
: h(ProjectBoardPane, {
project: state.project, rows: state.rows, canManage: state.canManage, tasks: state.tasks,
// The task's own board is where a row leads: the Tasks dialog reads `#task=` on mount and
// lands on that Bot or group. Nothing here re-decides which board that is.
onOpen: (board) => {
if (typeof window !== 'undefined') window.location.hash = `#task=${board.kind}:${board.id}`
actions?.openBoard?.()
setState(null)
},
}))))
}
24 changes: 24 additions & 0 deletions patches/ds-bot/files/test/ui-clickthrough.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,27 @@ test('#1347: the built bundle carries these rules, so a page reload shows them',
assert.ok(bundle.includes('bt-chat-strip-name'), 'client.js carries the blank-chat strip')
assert.ok(bundle.includes('Conversation copied as Markdown'), 'client.js carries the copy confirmation')
})

test('#1750: the Project board overlay covers the shell behind its scrim', () => {
const css = read('project-board.js')
// The overlay carried `bt-overlay`, a class no stylesheet defines: the board rendered as a short
// dialog in the page flow, with the bot tree and workflows legible under its rows and nothing
// dimming them. A modal covers the whole viewport, so the shell cannot be read as more board rows.
const overlay = rule(css, '.bt-project-overlay')
assert.match(overlay, /position:fixed/, 'the layer is taken out of the page flow')
assert.match(overlay, /inset:0/, 'and covers the viewport, not just its own content height')
assert.match(overlay, /align-items:center/, 'the dialog sits centred in that layer')
assert.match(overlay, /justify-content:center/)
// Every other modal in the client pairs a `bt-scrim` sibling with its dialog and dismisses on a
// click; the Project board was the one overlay with no scrim at all.
assert.match(css, /className: 'bt-scrim', onClick: close/, 'the scrim renders beside the dialog and closes the overlay')
// Reuse, not a second visual language: the dialog is kanban's own `bt-mem` surface at kanban's size.
const dialog = rule(css, '.bt-mem.bt-project-dialog')
assert.match(dialog, /width:min\(1080px,calc\(100vw - 32px\)\)/, 'the same dialog width as the Board')
assert.match(dialog, /height:min\(760px,calc\(100vh - 48px\)\)/, 'and the same height, so board rows are not mixed with the shell')

// The app runs client.js, so the rule only reaches the page once it is in the bundle.
const bundle = readFileSync(new URL('../client.js', import.meta.url), 'utf8')
assert.match(rule(bundle, '.bt-project-overlay'), /position:fixed;inset:0/, 'client.js carries the covering layer')
assert.ok(bundle.includes('bt-mem bt-project-dialog'), 'and the dialog it centres')
})
Loading