@unson/brainbase-mcp 0.41.10 → 0.41.11

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.
@@ -186,6 +186,21 @@ function makePage(container, context) {
186
186
  return page;
187
187
  }
188
188
 
189
+ /** Names of judgment references from the local Graph, under the host's current access. */
190
+ function judgmentReferenceLabelReader(context) {
191
+ const client = createGraphClient({ fetcher: context.fetcher, basePath: '/api/graph', token: context.token });
192
+ return async (references) => {
193
+ const labels = new Map();
194
+ await Promise.all(references.map(async ({ ref }) => {
195
+ const result = await client.read(`/entities/${encodeURIComponent(ref)}`);
196
+ const entity = result.state === 'ok' && isRecord(result.payload?.entity) ? result.payload.entity : null;
197
+ const name = entity && entity.id === ref && typeof entity.name === 'string' ? entity.name.trim() : '';
198
+ if (name) labels.set(ref, name);
199
+ }));
200
+ return labels;
201
+ };
202
+ }
203
+
189
204
  function mountToday(container, context) {
190
205
  return createJudgmentHistoryUI({
191
206
  root: makePage(container, context),
@@ -195,6 +210,7 @@ function mountToday(container, context) {
195
210
  fetcher: context.fetcher,
196
211
  token: context.token,
197
212
  basePath: '/api/judgment-history',
213
+ resolveReferenceLabels: judgmentReferenceLabelReader(context),
198
214
  });
199
215
  }
200
216
 
@@ -4,7 +4,7 @@
4
4
  --bb-color-accent-strong: var(--bb-color-brand-indigo);
5
5
  --bb-color-action: var(--bb-color-brand-indigo);
6
6
  display: grid;
7
- grid-template-rows: auto auto minmax(0, 1fr);
7
+ grid-template-rows: auto auto auto minmax(0, 1fr);
8
8
  gap: 4px;
9
9
  height: 100%;
10
10
  min-height: 0;
@@ -108,6 +108,70 @@
108
108
  color: var(--bb-color-danger);
109
109
  }
110
110
 
111
+ .bb-minutes-toolbar {
112
+ display: flex;
113
+ flex-wrap: wrap;
114
+ gap: 8px 12px;
115
+ align-items: flex-end;
116
+ min-width: 0;
117
+ padding: 8px 12px;
118
+ border: 1px solid var(--bb-color-border);
119
+ border-radius: var(--bb-radius-md, 6px);
120
+ background: var(--bb-color-surface);
121
+ }
122
+
123
+ .bb-minutes-filter-host {
124
+ display: contents;
125
+ }
126
+
127
+ .bb-minutes-filter-field {
128
+ display: grid;
129
+ gap: 2px;
130
+ min-width: 0;
131
+ }
132
+
133
+ .bb-minutes-filter-field[hidden],
134
+ .bb-minutes-filter-host[hidden],
135
+ .bb-minutes-filter-reset[hidden] {
136
+ display: none;
137
+ }
138
+
139
+ .bb-minutes-filter-field.is-keyword {
140
+ flex: 1 1 180px;
141
+ max-width: 260px;
142
+ }
143
+
144
+ .bb-minutes-filter-field .bb-minutes-input {
145
+ min-height: 38px;
146
+ padding: 4px 8px;
147
+ font-size: 13px;
148
+ }
149
+
150
+ .bb-minutes-filter-period {
151
+ display: flex;
152
+ gap: 4px;
153
+ align-items: center;
154
+ }
155
+
156
+ .bb-minutes-filter-period .bb-minutes-input {
157
+ width: 8.75rem;
158
+ }
159
+
160
+ .bb-minutes-filter-period-sep {
161
+ color: var(--bb-color-muted);
162
+ }
163
+
164
+ .bb-minutes-filter-combobox {
165
+ min-width: 11rem;
166
+ max-width: 16rem;
167
+ }
168
+
169
+ .bb-minutes-create-reason {
170
+ color: var(--bb-color-muted);
171
+ font-size: 12px;
172
+ overflow-wrap: anywhere;
173
+ }
174
+
111
175
  .bb-minutes-columns {
112
176
  display: grid;
113
177
  grid-template-columns: minmax(340px, 380px) minmax(0, 1fr);
@@ -537,6 +601,28 @@
537
601
  .bb-minutes-columns {
538
602
  grid-template-columns: 1fr;
539
603
  }
604
+
605
+ .bb-minutes-filter-field {
606
+ flex: 1 1 calc(50% - 6px);
607
+ max-width: none;
608
+ }
609
+
610
+ .bb-minutes-filter-field.is-keyword,
611
+ .bb-minutes-filter-field.is-period {
612
+ flex-basis: 100%;
613
+ max-width: none;
614
+ }
615
+
616
+ .bb-minutes-filter-period .bb-minutes-input {
617
+ flex: 1 1 0;
618
+ width: auto;
619
+ min-width: 0;
620
+ }
621
+
622
+ .bb-minutes-filter-combobox {
623
+ min-width: 0;
624
+ max-width: none;
625
+ }
540
626
  }
541
627
 
542
628
  /* この会議から: project, tasks, decisions, storage and sharing. */
@@ -7,12 +7,27 @@
7
7
  *
8
8
  * List order and labels are a projection. Canonical records are not rewritten.
9
9
  * Calendar links stay out until a meeting-side event id is verified.
10
+ * Filters and sort only narrow the loaded list; the host owns any filter that
11
+ * changes what it asks the server for (story-meeting-minutes-list-filters-v1).
10
12
  */
11
13
 
14
+ import { workspaceCombobox } from './workspace-kit.js';
15
+
12
16
  export const MEETING_MINUTES_UI_CONTRACT_VERSION = 'brainbase.meeting-minutes-ui.v1';
13
17
 
14
18
  const WEEKDAYS = ['日', '月', '火', '水', '木', '金', '土'];
15
19
  const TOKYO = 'Asia/Tokyo';
20
+ const SORTS = Object.freeze([
21
+ { value: 'scheduled-desc', label: '会議の日時が新しい順' },
22
+ { value: 'scheduled-asc', label: '会議の日時が古い順' },
23
+ { value: 'updated-desc', label: '議事録の更新が新しい順' },
24
+ ]);
25
+ const STATE_FILTERS = Object.freeze([
26
+ { value: '', label: 'すべての状態' },
27
+ { value: 'pending', label: '確認待ち' },
28
+ { value: 'confirmed', label: '確認済み' },
29
+ ]);
30
+ const EMPTY_FILTERS = Object.freeze({ query: '', from: '', to: '', participant: '', status: '' });
16
31
 
17
32
  function tokyoParts(date) {
18
33
  const parts = new Intl.DateTimeFormat('en-US', {
@@ -279,19 +294,71 @@ function byCreatedAt(versions) {
279
294
  .map((item) => item.version);
280
295
  }
281
296
 
282
- function sortMeetings(meetings) {
297
+ function updatedTime(entry) {
298
+ let latest = latestVersionTime(entry);
299
+ for (const documentRecord of Array.isArray(entry?.minutes) ? entry.minutes : []) {
300
+ const date = parseDate(documentRecord?.updated_at);
301
+ if (date && (!latest || date > latest)) latest = date;
302
+ }
303
+ return latest ?? importTime(entry);
304
+ }
305
+
306
+ function sortMeetings(meetings, sort = 'scheduled-desc') {
307
+ const byUpdate = sort === 'updated-desc';
308
+ const direction = sort === 'scheduled-asc' ? 1 : -1;
283
309
  return meetings
284
- .map((entry, index) => ({ entry, index, timing: meetingTiming(entry) }))
310
+ .map((entry, index) => {
311
+ const timing = meetingTiming(entry);
312
+ const updated = byUpdate ? updatedTime(entry) : null;
313
+ return {
314
+ entry,
315
+ index,
316
+ bottom: byUpdate ? !updated : timing.kind === 'unregistered',
317
+ at: byUpdate ? updated?.getTime() ?? Number.NEGATIVE_INFINITY : timing.at,
318
+ };
319
+ })
285
320
  .sort((left, right) => {
286
- const leftBottom = left.timing.kind === 'unregistered';
287
- const rightBottom = right.timing.kind === 'unregistered';
288
- if (leftBottom !== rightBottom) return leftBottom ? 1 : -1;
289
- if (left.timing.at !== right.timing.at) return right.timing.at - left.timing.at;
321
+ if (left.bottom !== right.bottom) return left.bottom ? 1 : -1;
322
+ // Meetings without a date keep the newest import first in every order.
323
+ if (left.at !== right.at) return left.bottom ? right.at - left.at : (left.at - right.at) * direction;
290
324
  return left.index - right.index;
291
325
  })
292
326
  .map((item) => item.entry);
293
327
  }
294
328
 
329
+ function searchText(value) {
330
+ return String(value ?? '').normalize('NFKC').toLocaleLowerCase('ja');
331
+ }
332
+
333
+ function tokyoDateValue(date) {
334
+ const { year, month, day } = tokyoParts(date);
335
+ return `${year}-${pad(month)}-${pad(day)}`;
336
+ }
337
+
338
+ function participantsOf(entry) {
339
+ const ids = entry?.meeting?.participant_ids;
340
+ return Array.isArray(ids) ? ids.map((id) => text(id)).filter(Boolean) : [];
341
+ }
342
+
343
+ function matchesFilters(entry, filters) {
344
+ const terms = searchText(filters.query).split(/\s+/u).filter(Boolean);
345
+ if (terms.length) {
346
+ const documents = Array.isArray(entry?.minutes) ? entry.minutes : [];
347
+ const haystack = searchText([entry?.meeting?.title, ...documents.map((documentRecord) => documentRecord?.title)].join('\n'));
348
+ if (!terms.every((term) => haystack.includes(term))) return false;
349
+ }
350
+ if (filters.from || filters.to) {
351
+ const timing = meetingTiming(entry);
352
+ if (!timing.date) return false;
353
+ const day = tokyoDateValue(timing.date);
354
+ if (filters.from && day < filters.from) return false;
355
+ if (filters.to && day > filters.to) return false;
356
+ }
357
+ if (filters.participant && !participantsOf(entry).includes(filters.participant)) return false;
358
+ if (filters.status && listState(entry)?.code !== filters.status) return false;
359
+ return true;
360
+ }
361
+
295
362
  function idempotencyKey() {
296
363
  const random = typeof globalThis.crypto?.randomUUID === 'function'
297
364
  ? globalThis.crypto.randomUUID()
@@ -570,7 +637,9 @@ export function createMeetingMinutesUI({
570
637
  meetingLabel,
571
638
  meetingConnections,
572
639
  canCreateMeeting,
640
+ createUnavailableReason,
573
641
  canEditMinutes,
642
+ hostFilters = [],
574
643
  currentPrincipalId,
575
644
  now = () => new Date(),
576
645
  } = {}) {
@@ -590,6 +659,8 @@ export function createMeetingMinutesUI({
590
659
  loading: false,
591
660
  listError: null,
592
661
  detailError: null,
662
+ filters: { ...EMPTY_FILTERS },
663
+ sort: SORTS[0].value,
593
664
  // Which document is being edited or about to be withdrawn; both reset on every detail load.
594
665
  editingMinutesId: null,
595
666
  withdrawingMinutesId: null,
@@ -598,6 +669,7 @@ export function createMeetingMinutesUI({
598
669
  };
599
670
  let listRequestGeneration = 0;
600
671
  let detailRequestGeneration = 0;
672
+ let createReasonSequence = 0;
601
673
  let connectionsSlot = null;
602
674
  let railProjectSlot = null;
603
675
  let railProjectMeeting = null;
@@ -610,6 +682,10 @@ export function createMeetingMinutesUI({
610
682
  makeElement(doc, 'p', { className: 'bb-minutes-lead', textContent: '新しい会議から読み、確認待ちを見つけて確認します。' }),
611
683
  );
612
684
  const status = makeElement(doc, 'p', { className: 'bb-minutes-status', attrs: { role: 'status', 'aria-live': 'polite' } });
685
+ const toolbar = makeElement(doc, 'div', {
686
+ className: 'bb-minutes-toolbar',
687
+ attrs: { role: 'search', 'aria-label': '議事録の絞り込みと並べ替え' },
688
+ });
613
689
  const columns = makeElement(doc, 'div', { className: 'bb-minutes-columns' });
614
690
  const listPane = makeElement(doc, 'section', {
615
691
  className: 'bb-minutes-list-pane',
@@ -617,9 +693,127 @@ export function createMeetingMinutesUI({
617
693
  });
618
694
  const detailPane = makeElement(doc, 'section', { className: 'bb-minutes-detail-pane', attrs: { 'aria-label': '議事録詳細' } });
619
695
  columns.append(listPane, detailPane);
620
- screen.append(heading, status, columns);
696
+ screen.append(heading, status, toolbar, columns);
621
697
  root.replaceChildren(screen);
622
698
 
699
+ function filterField(label, control, className = '') {
700
+ const field = makeElement(doc, 'div', { className: `bb-minutes-filter-field${className ? ` ${className}` : ''}` });
701
+ field.append(makeElement(doc, 'span', { className: 'bb-minutes-label', textContent: label }), control);
702
+ return field;
703
+ }
704
+
705
+ function filterCombobox(label, options, value, onChange) {
706
+ return workspaceCombobox(doc, { label, options, value, className: 'bb-minutes-filter-combobox', onChange });
707
+ }
708
+
709
+ const keyword = makeElement(doc, 'input', {
710
+ className: 'bb-minutes-input bb-minutes-filter-keyword',
711
+ type: 'search',
712
+ attrs: { 'aria-label': '会議名・議事録名で絞り込む', placeholder: '会議名・議事録名', autocomplete: 'off' },
713
+ });
714
+ keyword.addEventListener('input', () => { state.filters.query = keyword.value; renderList(); });
715
+ const fromInput = makeElement(doc, 'input', { className: 'bb-minutes-input', type: 'date', attrs: { 'aria-label': '期間の始め' } });
716
+ const toInput = makeElement(doc, 'input', { className: 'bb-minutes-input', type: 'date', attrs: { 'aria-label': '期間の終わり' } });
717
+ fromInput.addEventListener('change', () => { state.filters.from = fromInput.value; renderList(); });
718
+ toInput.addEventListener('change', () => { state.filters.to = toInput.value; renderList(); });
719
+ const period = makeElement(doc, 'span', { className: 'bb-minutes-filter-period' });
720
+ period.append(fromInput, makeElement(doc, 'span', { className: 'bb-minutes-filter-period-sep', textContent: '〜', attrs: { 'aria-hidden': 'true' } }), toInput);
721
+ const participantBox = filterCombobox('参加者', [{ value: '', label: 'すべての参加者' }], '', (value) => { state.filters.participant = value; renderList(); });
722
+ const statusBox = filterCombobox('確認の状態', STATE_FILTERS, '', (value) => { state.filters.status = value; renderList(); });
723
+ const sortBox = filterCombobox('並べ替え', SORTS, state.sort, (value) => { state.sort = value; renderList(); });
724
+ const hostSlot = makeElement(doc, 'div', { className: 'bb-minutes-filter-host' });
725
+ const participantField = filterField('参加者', participantBox.root);
726
+ const statusField = filterField('確認の状態', statusBox.root);
727
+ const reset = button(doc, '条件を外す', () => clearFilters(), { className: 'bb-minutes-button bb-minutes-filter-reset' });
728
+ toolbar.append(
729
+ filterField('キーワード', keyword, 'is-keyword'),
730
+ hostSlot,
731
+ filterField('期間', period, 'is-period'),
732
+ participantField,
733
+ statusField,
734
+ filterField('並べ替え', sortBox.root),
735
+ reset,
736
+ );
737
+ const hostBoxes = new Map();
738
+
739
+ function currentHostFilters() {
740
+ let filters = hostFilters;
741
+ if (typeof filters === 'function') {
742
+ try { filters = filters(); } catch { filters = []; }
743
+ }
744
+ return (Array.isArray(filters) ? filters : []).filter((filter) => isRecord(filter) && text(filter.id));
745
+ }
746
+
747
+ function clientFiltersActive() {
748
+ return Object.keys(EMPTY_FILTERS).some((key) => Boolean(state.filters[key]));
749
+ }
750
+
751
+ function filtersActive() {
752
+ return clientFiltersActive() || currentHostFilters().some((filter) => text(filter.value));
753
+ }
754
+
755
+ function clearFilters() {
756
+ state.filters = { ...EMPTY_FILTERS };
757
+ keyword.value = '';
758
+ fromInput.value = '';
759
+ toInput.value = '';
760
+ for (const filter of currentHostFilters()) {
761
+ if (text(filter.value) && typeof filter.onChange === 'function') filter.onChange('');
762
+ }
763
+ renderList();
764
+ }
765
+
766
+ function renderToolbar(meetings) {
767
+ const hosts = currentHostFilters();
768
+ const seen = new Set();
769
+ for (const filter of hosts) {
770
+ const id = text(filter.id);
771
+ seen.add(id);
772
+ const options = Array.isArray(filter.options) ? filter.options : [];
773
+ const value = text(filter.value);
774
+ let entry = hostBoxes.get(id);
775
+ if (!entry) {
776
+ const box = workspaceCombobox(doc, {
777
+ label: text(filter.label, id),
778
+ options,
779
+ value,
780
+ placeholder: text(filter.placeholder, '選択してください'),
781
+ className: 'bb-minutes-filter-combobox',
782
+ onChange: (next) => {
783
+ const current = currentHostFilters().find((candidate) => text(candidate.id) === id);
784
+ if (typeof current?.onChange === 'function') current.onChange(next);
785
+ },
786
+ });
787
+ entry = { box, field: filterField(text(filter.label, id), box.root) };
788
+ hostBoxes.set(id, entry);
789
+ }
790
+ entry.box.update({ options, value, disabled: filter.disabled === true });
791
+ }
792
+ for (const [id, entry] of hostBoxes) {
793
+ if (seen.has(id)) continue;
794
+ entry.box.destroy();
795
+ hostBoxes.delete(id);
796
+ }
797
+ hostSlot.replaceChildren(...hosts.map((filter) => hostBoxes.get(text(filter.id)).field));
798
+ hostSlot.hidden = hosts.length === 0;
799
+
800
+ // Offer only the conditions the loaded records can answer (P9).
801
+ const participants = [...new Set(meetings.flatMap(participantsOf))]
802
+ .sort((left, right) => left.localeCompare(right, 'ja', { numeric: true }));
803
+ if (state.filters.participant && !participants.includes(state.filters.participant)) state.filters.participant = '';
804
+ participantBox.update({
805
+ options: [{ value: '', label: 'すべての参加者' }, ...participants.map((participant) => ({ value: participant, label: participant }))],
806
+ value: state.filters.participant,
807
+ });
808
+ participantField.hidden = participants.length === 0;
809
+ const hasStates = meetings.some((entry) => listState(entry));
810
+ if (!hasStates) state.filters.status = '';
811
+ statusBox.update({ value: state.filters.status });
812
+ statusField.hidden = !hasStates;
813
+ sortBox.update({ value: state.sort });
814
+ reset.hidden = !filtersActive();
815
+ }
816
+
623
817
  function clock() {
624
818
  try {
625
819
  const value = typeof now === 'function' ? now() : new Date();
@@ -673,6 +867,34 @@ export function createMeetingMinutesUI({
673
867
  return canCreateMeeting !== false;
674
868
  }
675
869
 
870
+ /** One short reason from the host, shown beside a create button that cannot be pressed. */
871
+ function createReason() {
872
+ if (typeof createUnavailableReason === 'function') {
873
+ try {
874
+ return text(createUnavailableReason());
875
+ } catch {
876
+ return '';
877
+ }
878
+ }
879
+ return text(createUnavailableReason);
880
+ }
881
+
882
+ function appendCreateUnavailable(parent) {
883
+ const reason = createReason();
884
+ if (!reason) return;
885
+ const id = `bb-minutes-create-reason-${++createReasonSequence}`;
886
+ parent.append(
887
+ makeElement(doc, 'button', {
888
+ className: 'bb-minutes-button is-primary',
889
+ textContent: '会議を作成',
890
+ type: 'button',
891
+ disabled: true,
892
+ attrs: { type: 'button', 'aria-describedby': id },
893
+ }),
894
+ makeElement(doc, 'span', { className: 'bb-minutes-create-reason', textContent: reason, attrs: { id } }),
895
+ );
896
+ }
897
+
676
898
  function canEdit() {
677
899
  if (typeof canEditMinutes === 'function') {
678
900
  try {
@@ -840,7 +1062,7 @@ export function createMeetingMinutesUI({
840
1062
  parent.append(notice);
841
1063
  }
842
1064
 
843
- function renderMeetingRow(entry) {
1065
+ function renderMeetingRow(entry, { flat = false } = {}) {
844
1066
  const meeting = entry.meeting;
845
1067
  const timing = meetingTiming(entry);
846
1068
  const item = makeElement(doc, 'button', {
@@ -854,10 +1076,19 @@ export function createMeetingMinutesUI({
854
1076
  }));
855
1077
  const line = makeElement(doc, 'span', { className: 'bb-minutes-item-line' });
856
1078
  if (timing.kind === 'scheduled') {
857
- appendLinePart(line, makeElement(doc, 'span', { textContent: formatClock(timing.date) }));
1079
+ // Without date headings the row carries its own date.
1080
+ const when = flat ? `${formatMonthDay(timing.date)} ${formatClock(timing.date)}` : formatClock(timing.date);
1081
+ appendLinePart(line, makeElement(doc, 'span', { textContent: when }));
858
1082
  } else if (timing.kind === 'fallback' || (timing.kind === 'unregistered' && timing.date)) {
859
1083
  appendLinePart(line, makeElement(doc, 'span', { textContent: unscheduledLabel(timing) }));
860
1084
  }
1085
+ if (flat) {
1086
+ const updated = updatedTime(entry);
1087
+ appendLinePart(line, makeElement(doc, 'span', {
1088
+ className: 'bb-minutes-item-updated',
1089
+ textContent: updated ? `更新 ${formatMonthDay(updated)} ${formatClock(updated)}` : '更新日時は未記録',
1090
+ }));
1091
+ }
861
1092
  const label = meetingLabelText(meetingLabel, meeting);
862
1093
  if (label) appendLinePart(line, makeElement(doc, 'span', { className: 'bb-minutes-meeting-label', textContent: label }));
863
1094
  const rowState = listState(entry);
@@ -875,17 +1106,19 @@ export function createMeetingMinutesUI({
875
1106
  clear(listPane);
876
1107
  const head = makeElement(doc, 'div', { className: 'bb-minutes-pane-head' });
877
1108
  const meetings = state.meetings.filter((entry) => isRecord(entry?.meeting) && typeof entry.meeting.meeting_id === 'string');
1109
+ renderToolbar(meetings);
1110
+ const visible = sortMeetings(meetings.filter((entry) => matchesFilters(entry, state.filters)), state.sort);
878
1111
  if (canCreate()) {
879
1112
  head.append(button(doc, '会議を作成', () => renderCreateForm(), { className: 'bb-minutes-button is-primary' }));
1113
+ } else {
1114
+ appendCreateUnavailable(head);
880
1115
  }
881
- head.append(makeElement(doc, 'p', { className: 'bb-minutes-sort-note', textContent: `新しい順・${meetings.length}件` }));
1116
+ const narrowed = clientFiltersActive();
1117
+ head.append(makeElement(doc, 'p', {
1118
+ className: 'bb-minutes-sort-note',
1119
+ textContent: narrowed ? `${meetings.length}件中${visible.length}件` : `${meetings.length}件`,
1120
+ }));
882
1121
  listPane.append(head);
883
- if (!canCreate()) {
884
- listPane.append(makeElement(doc, 'p', {
885
- className: 'bb-minutes-create-guard',
886
- textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
887
- }));
888
- }
889
1122
  if (state.listError) {
890
1123
  appendLoadError(listPane, `会議一覧を読み込めませんでした。理由: ${state.listError}`, () => loadMeetings({ restoreFocus: true }));
891
1124
  }
@@ -893,14 +1126,27 @@ export function createMeetingMinutesUI({
893
1126
  if (state.listError) return;
894
1127
  listPane.append(makeElement(doc, 'p', {
895
1128
  className: 'bb-minutes-empty',
896
- textContent: 'まだ議事録はありません。会議を取り込むと、ここに新しい順で並びます。',
1129
+ textContent: filtersActive()
1130
+ ? '条件に合う会議はありません。'
1131
+ : 'まだ議事録はありません。会議を取り込むと、ここに新しい順で並びます。',
897
1132
  }));
1133
+ if (filtersActive()) listPane.append(button(doc, '条件を外す', () => clearFilters()));
1134
+ return;
1135
+ }
1136
+ if (visible.length === 0) {
1137
+ listPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '条件に合う会議はありません。' }));
1138
+ listPane.append(button(doc, '条件を外す', () => clearFilters()));
898
1139
  return;
899
1140
  }
900
1141
  const list = makeElement(doc, 'div', { className: 'bb-minutes-meeting-list' });
1142
+ if (state.sort === 'updated-desc') {
1143
+ for (const entry of visible) list.append(renderMeetingRow(entry, { flat: true }));
1144
+ listPane.append(list);
1145
+ return;
1146
+ }
901
1147
  const today = clock();
902
1148
  let currentKey = null;
903
- for (const entry of meetings) {
1149
+ for (const entry of visible) {
904
1150
  const timing = meetingTiming(entry);
905
1151
  if (timing.kind === 'unregistered') continue;
906
1152
  const key = dateKey(timing.date);
@@ -913,7 +1159,7 @@ export function createMeetingMinutesUI({
913
1159
  }
914
1160
  list.append(renderMeetingRow(entry));
915
1161
  }
916
- const unregistered = meetings.filter((entry) => meetingTiming(entry).kind === 'unregistered');
1162
+ const unregistered = visible.filter((entry) => meetingTiming(entry).kind === 'unregistered');
917
1163
  if (unregistered.length) {
918
1164
  list.append(makeElement(doc, 'h3', {
919
1165
  className: 'bb-minutes-date-heading',
@@ -1259,14 +1505,17 @@ export function createMeetingMinutesUI({
1259
1505
  }
1260
1506
  }
1261
1507
 
1508
+ function renderCreateUnavailable() {
1509
+ clear(detailPane);
1510
+ detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
1511
+ const reason = createReason();
1512
+ if (reason) detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-create-reason', textContent: reason }));
1513
+ }
1514
+
1262
1515
  function renderCreateForm() {
1263
1516
  clear(detailPane);
1264
1517
  if (!canCreate()) {
1265
- detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
1266
- detailPane.append(makeElement(doc, 'p', {
1267
- className: 'bb-minutes-create-guard',
1268
- textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
1269
- }));
1518
+ renderCreateUnavailable();
1270
1519
  return;
1271
1520
  }
1272
1521
  const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
@@ -1284,12 +1533,7 @@ export function createMeetingMinutesUI({
1284
1533
  if (!canCreate()) {
1285
1534
  save.disabled = true;
1286
1535
  renderList();
1287
- clear(detailPane);
1288
- detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
1289
- detailPane.append(makeElement(doc, 'p', {
1290
- className: 'bb-minutes-create-guard',
1291
- textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
1292
- }));
1536
+ renderCreateUnavailable();
1293
1537
  return;
1294
1538
  }
1295
1539
  save.disabled = true;
@@ -1403,6 +1647,7 @@ export function createMeetingMinutesUI({
1403
1647
  return state.detail;
1404
1648
  }
1405
1649
 
1650
+ renderToolbar([]);
1406
1651
  renderDetail(null);
1407
1652
  void loadMeetings();
1408
1653
  return {