@unson/brainbase-mcp 0.41.3 → 0.41.6
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/dist/judgment-history-http.js +52 -11
- package/dist/judgment-history.d.ts +70 -2
- package/dist/judgment-history.js +381 -122
- package/package.json +2 -2
- package/ui/brainbase-tokens.css +4 -0
- package/ui/foundation-overview.js +30 -11
- package/ui/judgment-history.css +250 -23
- package/ui/judgment-history.js +497 -115
- package/ui/local-web-shell.js +4 -1
- package/ui/people-and-companies-view.css +143 -0
- package/ui/people-and-companies-view.js +87 -11
- package/ui/workspace-kit.css +43 -0
- package/ui/workspace-kit.js +131 -4
package/ui/local-web-shell.js
CHANGED
|
@@ -451,8 +451,11 @@ export function createLocalWebShell({
|
|
|
451
451
|
workspace.append(mobileBar, main);
|
|
452
452
|
const railAside = makeElement(doc, 'aside', { className: 'bb-shell-rail', attrs: { 'aria-label': '選択中の項目' } });
|
|
453
453
|
const rails = new Map();
|
|
454
|
+
const nativeDrawers = Boolean(doc.body && typeof doc.createElement('dialog').showModal === 'function');
|
|
454
455
|
for (const screen of screens) {
|
|
455
|
-
|
|
456
|
+
// The built-in history uses a modal drawer in browsers, with a rail fallback
|
|
457
|
+
// for minimal hosts. Do not reserve an empty desktop column for the drawer.
|
|
458
|
+
if (!screen.rail || (nativeDrawers && screen.mount === mountToday)) continue;
|
|
456
459
|
const railSlot = makeElement(doc, 'div', { className: 'bb-shell-rail-slot', attrs: { 'data-rail': screen.id } });
|
|
457
460
|
railSlot.hidden = true;
|
|
458
461
|
rails.set(screen.id, railSlot);
|
|
@@ -119,3 +119,146 @@
|
|
|
119
119
|
display: none;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
/* Marks, counts and badges (2026-10). A person is a round mark, a company a
|
|
124
|
+
rounded square; both keep one tone per id. Counts sit next to each group
|
|
125
|
+
title, and the jump links at the top move to the people or company part. */
|
|
126
|
+
.bb-pc-jump {
|
|
127
|
+
display: flex;
|
|
128
|
+
gap: 4px;
|
|
129
|
+
margin: 4px 0 18px;
|
|
130
|
+
padding: 3px;
|
|
131
|
+
width: max-content;
|
|
132
|
+
border-radius: 9px;
|
|
133
|
+
background: var(--bb-color-canvas);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.bb-pc-jump-link {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 6px;
|
|
140
|
+
min-height: 28px;
|
|
141
|
+
padding: 0 12px;
|
|
142
|
+
border: 0;
|
|
143
|
+
border-radius: 6px;
|
|
144
|
+
background: transparent;
|
|
145
|
+
color: var(--bb-color-ink);
|
|
146
|
+
font: inherit;
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.bb-pc-jump-link:hover {
|
|
153
|
+
background: var(--bb-color-paper);
|
|
154
|
+
box-shadow: 0 1px 2px rgb(32 33 36 / 10%);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.bb-pc-count {
|
|
158
|
+
color: var(--bb-color-muted);
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
font-weight: 500;
|
|
161
|
+
font-variant-numeric: tabular-nums;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.bb-graph .bb-pc-group-title,
|
|
165
|
+
.bb-graph .bb-pc-company-title {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 8px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.bb-graph .bb-pc-company-title {
|
|
172
|
+
color: var(--bb-color-ink);
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.bb-pc-mark {
|
|
177
|
+
display: inline-grid;
|
|
178
|
+
flex: 0 0 auto;
|
|
179
|
+
place-items: center;
|
|
180
|
+
width: 24px;
|
|
181
|
+
height: 24px;
|
|
182
|
+
margin-right: 10px;
|
|
183
|
+
color: var(--bb-pc-mark-ink);
|
|
184
|
+
background: var(--bb-pc-mark-bg);
|
|
185
|
+
font-size: 11px;
|
|
186
|
+
font-weight: 700;
|
|
187
|
+
line-height: 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.bb-pc-person-mark { border-radius: 50%; }
|
|
191
|
+
.bb-pc-company-mark { border-radius: 6px; }
|
|
192
|
+
.bb-pc-company-title .bb-pc-mark,
|
|
193
|
+
.bb-pc-company-chip .bb-pc-mark { width: 18px; height: 18px; margin-right: 0; border-radius: 5px; font-size: 10px; }
|
|
194
|
+
|
|
195
|
+
.bb-pc-tone-0 { --bb-pc-mark-bg: #fde7e9; --bb-pc-mark-ink: #b4233a; }
|
|
196
|
+
.bb-pc-tone-1 { --bb-pc-mark-bg: #e3effd; --bb-pc-mark-ink: #1f5fbf; }
|
|
197
|
+
.bb-pc-tone-2 { --bb-pc-mark-bg: #e2f5ea; --bb-pc-mark-ink: #157046; }
|
|
198
|
+
.bb-pc-tone-3 { --bb-pc-mark-bg: #fff1d6; --bb-pc-mark-ink: #9a5a00; }
|
|
199
|
+
.bb-pc-tone-4 { --bb-pc-mark-bg: #efe7fd; --bb-pc-mark-ink: #6d36c9; }
|
|
200
|
+
.bb-pc-tone-5 { --bb-pc-mark-bg: #e0f4f6; --bb-pc-mark-ink: #0d6e7a; }
|
|
201
|
+
|
|
202
|
+
.bb-pc-people-ledger .bb-ws-ledger-object,
|
|
203
|
+
.bb-pc-company-ledger .bb-ws-ledger-object {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
min-width: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.bb-pc-people-ledger .bb-ws-ledger-object strong,
|
|
210
|
+
.bb-pc-company-ledger .bb-ws-ledger-object strong {
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
font-weight: 600;
|
|
213
|
+
text-overflow: ellipsis;
|
|
214
|
+
white-space: nowrap;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.bb-pc-company-chips {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
gap: 4px 12px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.bb-pc-company-chip {
|
|
224
|
+
display: inline-flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 7px;
|
|
227
|
+
min-width: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.bb-pc-kinds {
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-wrap: wrap;
|
|
233
|
+
gap: 4px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.bb-pc-kind {
|
|
237
|
+
display: inline-flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 5px;
|
|
240
|
+
padding: 1px 8px;
|
|
241
|
+
border-radius: 5px;
|
|
242
|
+
background: var(--bb-color-canvas);
|
|
243
|
+
color: var(--bb-color-muted);
|
|
244
|
+
font-size: 12px;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.bb-pc-kind::before {
|
|
249
|
+
content: '';
|
|
250
|
+
width: 6px;
|
|
251
|
+
height: 6px;
|
|
252
|
+
border-radius: 50%;
|
|
253
|
+
background: currentColor;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.bb-pc-kind-customer { background: #e2f5ea; color: #157046; }
|
|
257
|
+
.bb-pc-kind-partner { background: #e3effd; color: #1f5fbf; }
|
|
258
|
+
.bb-pc-kind-prospect { background: #fff1d6; color: #9a5a00; }
|
|
259
|
+
|
|
260
|
+
.bb-pc-people-ledger .bb-ws-ledger-row:not(.bb-ws-ledger-head),
|
|
261
|
+
.bb-pc-company-ledger .bb-ws-ledger-row:not(.bb-ws-ledger-head) {
|
|
262
|
+
align-items: center;
|
|
263
|
+
min-height: 46px;
|
|
264
|
+
}
|
|
@@ -117,6 +117,21 @@ function ownKey(table, key) {
|
|
|
117
117
|
return typeof key === 'string' && Object.hasOwn(table, key) ? table[key] : undefined;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** First visible character of a name for its mark, or a placeholder. */
|
|
121
|
+
function nameInitial(name) {
|
|
122
|
+
const text = String(name ?? '').trim();
|
|
123
|
+
const latin = text.split(/\s+/u).filter(Boolean);
|
|
124
|
+
if (/^[A-Za-z]/u.test(text) && latin.length > 1) return `${latin[0][0]}${latin[1][0]}`.toUpperCase();
|
|
125
|
+
return ([...text][0] ?? '?').toUpperCase();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A stable tone (0-5) per id so a person or company keeps its color. */
|
|
129
|
+
function markTone(id) {
|
|
130
|
+
let hash = 0;
|
|
131
|
+
for (const character of String(id ?? '')) hash = (hash * 31 + character.codePointAt(0)) >>> 0;
|
|
132
|
+
return hash % 6;
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
/** The read is `ok` only when every person and company is well formed. */
|
|
121
136
|
export function normalizePeopleAndCompanies(result) {
|
|
122
137
|
if (result?.state !== 'ok') return result ?? INVALID;
|
|
@@ -269,8 +284,8 @@ export function createPeopleAndCompaniesView({
|
|
|
269
284
|
selected: person.id === state.detail?.id,
|
|
270
285
|
onSelect: (id) => void controller.openEntity(id),
|
|
271
286
|
cells: [
|
|
272
|
-
{ primary: person.isOwner ? `${person.name}(あなた)` : person.name,
|
|
273
|
-
person.companyIds.length > 0 ? person.companyIds
|
|
287
|
+
{ primary: person.isOwner ? `${person.name}(あなた)` : person.name, leading: personMark(person) },
|
|
288
|
+
person.companyIds.length > 0 ? companyChips(person.companyIds) : companyCell(person, ownId),
|
|
274
289
|
person.accountableProjectCount > 0
|
|
275
290
|
? `${person.projectCount}件(うち責任 ${person.accountableProjectCount}件)`
|
|
276
291
|
: `${person.projectCount}件`,
|
|
@@ -280,6 +295,67 @@ export function createPeopleAndCompaniesView({
|
|
|
280
295
|
}));
|
|
281
296
|
}
|
|
282
297
|
|
|
298
|
+
function personMark(person) {
|
|
299
|
+
return makeElement(doc, 'span', {
|
|
300
|
+
className: `bb-pc-mark bb-pc-person-mark bb-pc-tone-${markTone(person.id)}`,
|
|
301
|
+
text: nameInitial(person.name),
|
|
302
|
+
attrs: { 'aria-hidden': 'true' },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function companyMark(id, name) {
|
|
307
|
+
return makeElement(doc, 'span', {
|
|
308
|
+
className: `bb-pc-mark bb-pc-company-mark bb-pc-tone-${markTone(id)}`,
|
|
309
|
+
text: nameInitial(name),
|
|
310
|
+
attrs: { 'aria-hidden': 'true' },
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function companyChips(ids) {
|
|
315
|
+
const list = makeElement(doc, 'span', { className: 'bb-pc-company-chips' });
|
|
316
|
+
for (const id of ids) {
|
|
317
|
+
const chip = makeElement(doc, 'span', { className: 'bb-pc-company-chip' });
|
|
318
|
+
chip.append(companyMark(id, nameOf(id)), makeElement(doc, 'span', { text: nameOf(id) }));
|
|
319
|
+
list.append(chip);
|
|
320
|
+
}
|
|
321
|
+
return list;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function kindBadges(company) {
|
|
325
|
+
const kinds = company.kinds ?? [];
|
|
326
|
+
if (kinds.length === 0) return '';
|
|
327
|
+
const list = makeElement(doc, 'span', { className: 'bb-pc-kinds' });
|
|
328
|
+
for (const item of kinds) {
|
|
329
|
+
list.append(makeElement(doc, 'span', {
|
|
330
|
+
className: `bb-pc-kind bb-pc-kind-${Object.hasOwn(COMPANY_KIND_LABELS, item.kind) ? item.kind : 'other'}`,
|
|
331
|
+
text: ownKey(COMPANY_KIND_LABELS, item.kind) ?? item.kind,
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
return list;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function groupTitle(tag, text, count, { className = 'bb-pc-group-title', leading } = {}) {
|
|
338
|
+
const title = makeElement(doc, tag, { className });
|
|
339
|
+
if (leading) title.append(leading);
|
|
340
|
+
title.append(makeElement(doc, 'span', { text }), makeElement(doc, 'span', { className: 'bb-pc-count', text: String(count) }));
|
|
341
|
+
return title;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function jumpLinks(payload) {
|
|
345
|
+
const people = payload.people.filter((person) => person.active).length;
|
|
346
|
+
const companies = payload.companies.filter((company) => company.active).length;
|
|
347
|
+
const nav = makeElement(doc, 'nav', { className: 'bb-pc-jump', attrs: { 'aria-label': '人と会社の段' } });
|
|
348
|
+
for (const [label, count, target] of [['人', people, 'bb-pc-people'], ['会社', companies, 'bb-pc-companies']]) {
|
|
349
|
+
const link = button(doc, '', () => {
|
|
350
|
+
const node = typeof doc.getElementById === 'function' ? doc.getElementById(target) : null;
|
|
351
|
+
node?.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
|
|
352
|
+
}, { className: 'bb-pc-jump-link', attrs: { 'aria-label': `${label} ${count}件の段へ移る` } });
|
|
353
|
+
link.append(makeElement(doc, 'span', { text: label }), makeElement(doc, 'span', { className: 'bb-pc-count', text: String(count) }));
|
|
354
|
+
nav.append(link);
|
|
355
|
+
}
|
|
356
|
+
return nav;
|
|
357
|
+
}
|
|
358
|
+
|
|
283
359
|
function orgNoteText(note) {
|
|
284
360
|
return `記録上のメモ: ${note}(所属ではありません)`;
|
|
285
361
|
}
|
|
@@ -324,7 +400,7 @@ export function createPeopleAndCompaniesView({
|
|
|
324
400
|
}
|
|
325
401
|
|
|
326
402
|
function peopleSection(payload) {
|
|
327
|
-
const section = makeElement(doc, 'section', { className: 'bb-pc-people', attrs: { 'aria-label': '人' } });
|
|
403
|
+
const section = makeElement(doc, 'section', { className: 'bb-pc-people', attrs: { 'aria-label': '人', id: 'bb-pc-people' } });
|
|
328
404
|
section.append(workspaceSectionTitle(doc, { title: '人', lead: '社内か社外かは、所属(会社への所属の関係)と自社から決めています。印は別に持っていません。' }));
|
|
329
405
|
if (can.createPerson) section.append(sectionActions('人を加える', () => openCreateEntity('person')));
|
|
330
406
|
const groups = groupPeople(payload);
|
|
@@ -337,22 +413,22 @@ export function createPeopleAndCompaniesView({
|
|
|
337
413
|
}));
|
|
338
414
|
}
|
|
339
415
|
if (groups.ownCompanyId) {
|
|
340
|
-
section.append(
|
|
416
|
+
section.append(groupTitle('h3', `社内(${nameOf(groups.ownCompanyId)})`, groups.internal.length));
|
|
341
417
|
section.append(peopleLedger('社内の人', groups.internal, '自社に所属する人の記録はありません。'));
|
|
342
|
-
section.append(
|
|
418
|
+
section.append(groupTitle('h3', '社外', groups.external.reduce((sum, group) => sum + group.people.length, 0)));
|
|
343
419
|
if (groups.external.length === 0) section.append(makeElement(doc, 'p', { className: 'bb-pc-empty', text: 'ほかの会社に所属する人の記録はありません。' }));
|
|
344
420
|
for (const group of groups.external) {
|
|
345
|
-
section.append(
|
|
421
|
+
section.append(groupTitle('h4', group.company.name, group.people.length, { className: 'bb-pc-company-title', leading: companyMark(group.company.id, group.company.name) }));
|
|
346
422
|
section.append(peopleLedger(`${group.company.name}の人`, group.people));
|
|
347
423
|
}
|
|
348
424
|
}
|
|
349
|
-
section.append(
|
|
425
|
+
section.append(groupTitle('h3', '所属未確認', groups.unknown.length));
|
|
350
426
|
section.append(peopleLedger('所属未確認の人', groups.unknown, '所属が分からない人はいません。'));
|
|
351
427
|
return section;
|
|
352
428
|
}
|
|
353
429
|
|
|
354
430
|
function companySection(payload) {
|
|
355
|
-
const section = makeElement(doc, 'section', { className: 'bb-pc-companies', attrs: { 'aria-label': '会社' } });
|
|
431
|
+
const section = makeElement(doc, 'section', { className: 'bb-pc-companies', attrs: { 'aria-label': '会社', id: 'bb-pc-companies' } });
|
|
356
432
|
section.append(workspaceSectionTitle(doc, {
|
|
357
433
|
title: '会社',
|
|
358
434
|
lead: '自社を先頭に、顧客・協業先など関わる会社を並べています。種類は、会社の属性、プロジェクトとの関係の役割、提案中の案件の記録から出しています。記録が無い会社には種類を出しません。',
|
|
@@ -369,8 +445,8 @@ export function createPeopleAndCompaniesView({
|
|
|
369
445
|
selected: company.id === state.detail?.id,
|
|
370
446
|
onSelect: (id) => void controller.openEntity(id),
|
|
371
447
|
cells: [
|
|
372
|
-
{ primary: company.id === payload.ownCompany.id ? `${company.name}(自社)` : company.name,
|
|
373
|
-
|
|
448
|
+
{ primary: company.id === payload.ownCompany.id ? `${company.name}(自社)` : company.name, leading: companyMark(company.id, company.name) },
|
|
449
|
+
kindBadges(company),
|
|
374
450
|
`${company.memberIds.length}人`,
|
|
375
451
|
company.relatedProjects.length > 0
|
|
376
452
|
? `${company.projectIds.length}件(関わる ${company.relatedProjects.length}件)`
|
|
@@ -408,7 +484,7 @@ export function createPeopleAndCompaniesView({
|
|
|
408
484
|
return [...children, graphHostEmptyNotice(doc, emptyNotice)
|
|
409
485
|
?? workspaceNotice(doc, { label: '未登録', tone: 'info', role: 'status', text: 'Graphに人と会社の記録はまだありません。' })];
|
|
410
486
|
}
|
|
411
|
-
return [...children, peopleSection(payload), companySection(payload), endedNote(payload)];
|
|
487
|
+
return [...children, jumpLinks(payload), peopleSection(payload), companySection(payload), endedNote(payload)];
|
|
412
488
|
}
|
|
413
489
|
|
|
414
490
|
// -------------------------------------------------------------------------
|
package/ui/workspace-kit.css
CHANGED
|
@@ -530,3 +530,46 @@
|
|
|
530
530
|
.bb-ws-error-card :is(summary,button):focus-visible { outline: 2px solid var(--bb-color-notice-text); outline-offset: 2px; }
|
|
531
531
|
.bb-ws-inline-error { height: 15px; max-height: 15px; margin: 4px 0; padding: 0; overflow: hidden; color: var(--bb-color-inline-error); font-size: 11px; line-height: 15px; }
|
|
532
532
|
@media (prefers-reduced-motion: reduce) { .bb-ws-help-bubble { transition: none; } }
|
|
533
|
+
|
|
534
|
+
/* One native-dialog drawer presentation for shared parts and host-owned task dialogs. */
|
|
535
|
+
.bb-ws-drawer {
|
|
536
|
+
box-sizing: border-box; width: min(640px,100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh;
|
|
537
|
+
margin: 0 0 0 auto; padding: 0; border: 0; border-left: 1px solid var(--bb-color-border);
|
|
538
|
+
border-radius: 0; background: var(--bb-color-panel); color: var(--bb-color-ink); box-shadow: var(--bb-shadow-drawer); overflow: auto;
|
|
539
|
+
}
|
|
540
|
+
.bb-ws-drawer[open] { animation: bb-ws-drawer-in 180ms ease-out; }
|
|
541
|
+
.bb-ws-drawer::backdrop { background: var(--bb-color-drawer-overlay); }
|
|
542
|
+
.bb-ws-drawer.is-closing { animation: bb-ws-drawer-out 280ms ease-in both; pointer-events: none; }
|
|
543
|
+
.bb-ws-drawer.is-closing::backdrop { animation: bb-ws-drawer-backdrop-out 280ms ease-in both; }
|
|
544
|
+
.bb-ws-drawer .bb-ws-dialog-head { align-items: center; padding: 14px 16px 14px 28px; }
|
|
545
|
+
.bb-ws-drawer .bb-ws-dialog-head h2 { font-weight: 650; line-height: 1.35; }
|
|
546
|
+
.bb-ws-drawer .bb-ws-dialog-body { padding: 18px 28px 32px; }
|
|
547
|
+
@keyframes bb-ws-drawer-in { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
548
|
+
@keyframes bb-ws-drawer-out { from { transform: translateX(0); opacity: 1; } to { transform: translateX(32px); opacity: 0; } }
|
|
549
|
+
@keyframes bb-ws-drawer-backdrop-out { from { background: var(--bb-color-drawer-overlay); } to { background: var(--bb-color-drawer-overlay-clear); } }
|
|
550
|
+
.bb-ws-filter-anchor { position: relative; display: inline-flex; flex: none; }
|
|
551
|
+
.bb-ws-filter-anchor > .bb-ws-button::after { content: '⌄'; margin-left: var(--bb-space-2); }
|
|
552
|
+
.bb-ws-filter-popover {
|
|
553
|
+
position: absolute; z-index: 40; top: calc(100% + 8px); right: 0; display: grid; gap: var(--bb-space-4);
|
|
554
|
+
box-sizing: border-box; width: 360px; max-width: calc(100vw - 48px); padding: 20px;
|
|
555
|
+
border: 1px solid var(--bb-color-border); border-radius: 12px; background: var(--bb-color-panel); color: var(--bb-color-ink);
|
|
556
|
+
box-shadow: var(--bb-shadow-popover); max-height: calc(100dvh - 220px); overflow-y: auto;
|
|
557
|
+
transform-origin: top right; animation: bb-ws-filter-enter 180ms ease-out;
|
|
558
|
+
}
|
|
559
|
+
.bb-ws-filter-popover[hidden] { display: none !important; }
|
|
560
|
+
@keyframes bb-ws-filter-enter { from { opacity: 0; transform: translateY(-6px) scaleY(.96); } to { opacity: 1; transform: none; } }
|
|
561
|
+
.bb-ws-loading-card { align-items: center; border-left-color: var(--bb-color-notice-control-border); }
|
|
562
|
+
.bb-ws-loading-spinner { position: relative; display: block; flex: none; width: 14px; height: 14px; }
|
|
563
|
+
.bb-ws-loading-spinner::before {
|
|
564
|
+
content: ''; position: absolute; top: 1px; left: 1px; box-sizing: border-box; width: 12px; height: 12px;
|
|
565
|
+
border: 1.5px solid var(--bb-color-notice-control-border); border-top-color: var(--bb-color-notice-text); border-radius: 50%;
|
|
566
|
+
transform-origin: center; animation: bb-ws-spinner 1s linear infinite;
|
|
567
|
+
}
|
|
568
|
+
@keyframes bb-ws-spinner { to { transform: rotate(360deg); } }
|
|
569
|
+
.bb-ws-error-card.is-dismissing { animation: bb-ws-notification-out 450ms ease both; pointer-events: none; }
|
|
570
|
+
@keyframes bb-ws-notification-out { to { opacity: 0; transform: translateX(28px); } }
|
|
571
|
+
@media (max-width:760px) { .bb-ws-drawer { width: 100vw; border-left: 0; } }
|
|
572
|
+
@media (prefers-reduced-motion:reduce) {
|
|
573
|
+
.bb-ws-drawer[open], .bb-ws-drawer.is-closing, .bb-ws-drawer.is-closing::backdrop,
|
|
574
|
+
.bb-ws-filter-popover, .bb-ws-loading-spinner::before, .bb-ws-error-card.is-dismissing { animation: none; }
|
|
575
|
+
}
|
package/ui/workspace-kit.js
CHANGED
|
@@ -256,23 +256,150 @@ export function workspaceHelp(doc, { text, label } = {}) {
|
|
|
256
256
|
wrap.append(control, bubble); return wrap;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
const drawerClosings = new WeakMap();
|
|
260
|
+
const notificationClosings = new WeakMap();
|
|
261
|
+
const reducedMotion = node => node.ownerDocument?.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
|
|
262
|
+
|
|
263
|
+
/** Apply the same native-dialog presentation to host-owned and kit drawers. */
|
|
264
|
+
export function workspaceApplyDrawer(dialog) {
|
|
265
|
+
dialog.classList.add('bb-ws-drawer');
|
|
266
|
+
return dialog;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Keep the modal/focus trap alive until the exit motion finishes. Returns cancellation cleanup. */
|
|
270
|
+
export function workspaceCloseDrawer(dialog, finish) {
|
|
271
|
+
if (drawerClosings.has(dialog)) return drawerClosings.get(dialog);
|
|
272
|
+
if (!dialog.open || reducedMotion(dialog)) { finish(); return () => {}; }
|
|
273
|
+
let timer;
|
|
274
|
+
let active = true;
|
|
275
|
+
const cancel = () => {
|
|
276
|
+
if (!active) return;
|
|
277
|
+
active = false;
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
dialog.removeEventListener('animationend', ended);
|
|
280
|
+
dialog.removeEventListener('close', cancel);
|
|
281
|
+
dialog.classList.remove('is-closing');
|
|
282
|
+
drawerClosings.delete(dialog);
|
|
283
|
+
};
|
|
284
|
+
const complete = () => { if (!active) return; cancel(); finish(); };
|
|
285
|
+
const ended = event => {
|
|
286
|
+
if (event.target === dialog && event.animationName === 'bb-ws-drawer-out') complete();
|
|
287
|
+
};
|
|
288
|
+
drawerClosings.set(dialog, cancel);
|
|
289
|
+
dialog.addEventListener('animationend', ended);
|
|
290
|
+
dialog.addEventListener('close', cancel);
|
|
291
|
+
dialog.classList.add('is-closing');
|
|
292
|
+
timer = setTimeout(complete, 360); // Animation cancellation / background-tab fallback.
|
|
293
|
+
return cancel;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Native modal, optionally a right drawer. close() animates; dispose() tears down immediately. */
|
|
297
|
+
export function workspaceDialog(doc, { title, content, opener, presentation = 'center', onClose } = {}) {
|
|
261
298
|
const dialog = el(doc, 'dialog', { className: 'bb-ws-dialog' });
|
|
299
|
+
if (presentation === 'drawer') workspaceApplyDrawer(dialog);
|
|
262
300
|
const headingId = `bb-ws-dialog-${++workspaceSequence}`;
|
|
263
301
|
dialog.setAttribute('aria-labelledby', headingId);
|
|
264
302
|
const head = el(doc, 'header', { className: 'bb-ws-dialog-head' });
|
|
303
|
+
const nativeClose = dialog.close.bind(dialog);
|
|
304
|
+
let disposed = false;
|
|
305
|
+
const cleanup = () => {
|
|
306
|
+
if (disposed) return;
|
|
307
|
+
disposed = true;
|
|
308
|
+
drawerClosings.get(dialog)?.();
|
|
309
|
+
dialog.remove();
|
|
310
|
+
try { onClose?.(); } finally {
|
|
311
|
+
const target = typeof opener === 'function' ? opener() : opener;
|
|
312
|
+
if (target?.isConnected) target.focus();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
dialog.close = value => {
|
|
316
|
+
if (disposed) return;
|
|
317
|
+
const finish = () => { nativeClose(value); cleanup(); };
|
|
318
|
+
if (presentation === 'drawer') workspaceCloseDrawer(dialog, finish);
|
|
319
|
+
else finish();
|
|
320
|
+
};
|
|
321
|
+
dialog.dispose = () => {
|
|
322
|
+
if (disposed) return;
|
|
323
|
+
drawerClosings.get(dialog)?.();
|
|
324
|
+
nativeClose(); cleanup();
|
|
325
|
+
};
|
|
265
326
|
const close = workspaceButton(doc, { text: '×', attrs: { 'aria-label': '閉じる' }, onClick: () => dialog.close() });
|
|
266
327
|
head.append(el(doc, 'h2', { text: title, attrs: { id: headingId } }), close);
|
|
267
328
|
const body = el(doc, 'div', { className: 'bb-ws-dialog-body' }); appendContent(body, doc, content);
|
|
268
329
|
dialog.append(head, body); doc.body.append(dialog);
|
|
330
|
+
dialog.addEventListener('cancel', event => { event.preventDefault(); dialog.close(); });
|
|
269
331
|
dialog.addEventListener('click', event => {
|
|
270
332
|
if (event.target !== dialog) return;
|
|
271
333
|
const rect = dialog.getBoundingClientRect();
|
|
272
334
|
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
|
|
273
335
|
});
|
|
274
|
-
dialog.addEventListener('close',
|
|
275
|
-
dialog.showModal(); close.focus();
|
|
336
|
+
dialog.addEventListener('close', cleanup, { once: true });
|
|
337
|
+
try { dialog.showModal(); close.focus(); } catch (error) { cleanup(); throw error; }
|
|
338
|
+
return dialog;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Anchored advanced search. Hosts destroy it before replacing its controls. */
|
|
342
|
+
export function workspaceFilterPopover(doc, { content, open = false, onToggle, trigger, panel: suppliedPanel, initialFocus } = {}) {
|
|
343
|
+
const root = el(doc, 'span', { className: 'bb-ws-filter-anchor' });
|
|
344
|
+
const panel = suppliedPanel || el(doc, 'div', { className: 'bb-ws-filter-popover', attrs: { id: `bb-ws-filter-${++workspaceSequence}`, role: 'region', 'aria-label': '詳細検索' } });
|
|
345
|
+
if (!panel.id && !panel.getAttribute?.('id')) panel.setAttribute('id', `bb-ws-filter-${++workspaceSequence}`);
|
|
346
|
+
panel.classList?.add('bb-ws-filter-popover');
|
|
347
|
+
const control = trigger || workspaceButton(doc, { text: '詳細検索', variant: 'quiet' });
|
|
348
|
+
control.setAttribute('aria-controls', panel.id || panel.getAttribute?.('id') || panel.attributes?.id);
|
|
349
|
+
appendContent(panel, doc, content); root.append(control, panel);
|
|
350
|
+
let destroyed = false;
|
|
351
|
+
const removeListeners = () => { doc.removeEventListener?.('pointerdown', outside); doc.removeEventListener?.('keydown', escape); };
|
|
352
|
+
const setOpen = (value, focus = false) => {
|
|
353
|
+
if (destroyed) return;
|
|
354
|
+
removeListeners(); panel.hidden = !value; control.setAttribute('aria-expanded', String(value));
|
|
355
|
+
onToggle?.(value);
|
|
356
|
+
if (value) {
|
|
357
|
+
doc.addEventListener?.('pointerdown', outside); doc.addEventListener?.('keydown', escape);
|
|
358
|
+
if (focus) (typeof initialFocus === 'function' ? initialFocus() : initialFocus || panel.querySelector?.('input:not(:disabled),select:not(:disabled),button:not(:disabled)'))?.focus();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const outside = event => { if (!root.contains?.(event.target)) setOpen(false); };
|
|
362
|
+
const escape = event => {
|
|
363
|
+
if (event.key !== 'Escape') return;
|
|
364
|
+
event.preventDefault(); event.stopPropagation(); setOpen(false); control.focus();
|
|
365
|
+
};
|
|
366
|
+
const toggle = () => setOpen(panel.hidden, true);
|
|
367
|
+
control.addEventListener('click', toggle); setOpen(open);
|
|
368
|
+
return { root, control, panel, destroy() {
|
|
369
|
+
destroyed = true; removeListeners(); control.removeEventListener?.('click', toggle);
|
|
370
|
+
panel.hidden = true; control.setAttribute('aria-expanded', 'false');
|
|
371
|
+
} };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Compact notification with a fixed-center, purely visual spinner. */
|
|
375
|
+
export function workspaceLoadingCard(doc, { title = 'ロード中' } = {}) {
|
|
376
|
+
const card = el(doc, 'section', { className: 'bb-ws-error-card bb-ws-loading-card', attrs: { role: 'status', 'aria-live': 'polite' } });
|
|
377
|
+
card.append(el(doc, 'span', { className: 'bb-ws-loading-spinner', attrs: { 'aria-hidden': 'true' } }), el(doc, 'span', { text: title }));
|
|
378
|
+
return card;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Fade right then remove. Calling the returned cancel function restores a reusable card. */
|
|
382
|
+
export function workspaceDismissNotification(card, onDismiss) {
|
|
383
|
+
const remove = () => {
|
|
384
|
+
if (typeof card.remove === 'function') card.remove();
|
|
385
|
+
else if (card.parentNode?.removeChild) card.parentNode.removeChild(card);
|
|
386
|
+
else if (card.parentNode?.replaceChildren) card.parentNode.replaceChildren(...Array.from(card.parentNode.children).filter(node => node !== card));
|
|
387
|
+
};
|
|
388
|
+
if (notificationClosings.has(card)) return notificationClosings.get(card);
|
|
389
|
+
if (!card.classList || reducedMotion(card)) { remove(); onDismiss?.(); return () => {}; }
|
|
390
|
+
let timer;
|
|
391
|
+
let active = true;
|
|
392
|
+
const cancel = () => {
|
|
393
|
+
if (!active) return;
|
|
394
|
+
active = false; clearTimeout(timer);
|
|
395
|
+
card.removeEventListener('animationend', ended);
|
|
396
|
+
card.classList.remove('is-dismissing'); notificationClosings.delete(card);
|
|
397
|
+
};
|
|
398
|
+
const complete = () => { if (!active) return; cancel(); remove(); onDismiss?.(); };
|
|
399
|
+
const ended = event => { if (event.target === card && event.animationName === 'bb-ws-notification-out') complete(); };
|
|
400
|
+
notificationClosings.set(card, cancel); card.addEventListener('animationend', ended);
|
|
401
|
+
card.classList.add('is-dismissing'); timer = setTimeout(complete, 500);
|
|
402
|
+
return cancel;
|
|
276
403
|
}
|
|
277
404
|
|
|
278
405
|
/** Persistent, initially closed notification. The caller owns its lifecycle. */
|