clavarium 0.2.378-next.1 → 0.2.379-next.1
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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/door.mjs +2 -1
- package/server/settingswords.mjs +8 -0
- package/web/app.js +124 -145
- package/web/device.html +13 -2
- package/web/kit.js +48 -12
- package/web/style.css +37 -0
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.379-next.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clavarium",
|
|
9
|
-
"version": "0.2.
|
|
9
|
+
"version": "0.2.379-next.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/package.json
CHANGED
package/server/door.mjs
CHANGED
|
@@ -153,7 +153,8 @@ export async function handleDoor(request, response, { devices, conversations, re
|
|
|
153
153
|
if (request.method === 'GET' && route === '/me') {
|
|
154
154
|
/* `conversations`: all this pairing may use. `shared`: what it was given for itself. */
|
|
155
155
|
/* `terminal` and `chat`: the parts it was given, for the page to show only those. */
|
|
156
|
-
|
|
156
|
+
/* Each attached one with its type: the page draws it as the asset it is (assets step 6). */
|
|
157
|
+
const about = mine ? { name: mine.name, toolLabel: mine.toolLabel, chatName: mine.chatName, voiceMark: mine.voiceMark, phase: mine.phase, step: mine.step, progress: mine.progress, error: mine.error, work: mine.work, resources: resources.of(name).map((r) => ({ ...r, type: resources.get(r.name)?.type ?? 'folder' })), ...part } : { name: null, resources: [] };
|
|
157
158
|
return send(200, { ...about, conversations: names.filter((n) => conversations.list().some((c) => c.name === n)), shared: own });
|
|
158
159
|
}
|
|
159
160
|
/* The files a device was given for itself need no conversation; everything else does. */
|
package/server/settingswords.mjs
CHANGED
|
@@ -189,5 +189,13 @@ export const SETTINGS_WORDS_FR = {
|
|
|
189
189
|
'set-room-people': 'Personnes', 'set-room-ais-of': 'IA', 'set-room-boxes-of': 'Boîtiers', 'set-room-host-mark': 'accueille',
|
|
190
190
|
'set-room-n-person': '1 personne', 'set-room-n-people': '{n} personnes', 'set-room-n-ai': '1 IA', 'set-room-n-ais': '{n} IA',
|
|
191
191
|
'set-room-n-box': '1 boîtier', 'set-room-n-boxes': '{n} boîtiers',
|
|
192
|
+
/* Devices on the same pieces (assets step 6): /c/'s Files, a device's panel, by thing, the share basket. */
|
|
193
|
+
'set-dev-given': 'Donné à cet appareil', 'set-dev-with': 'Avec {name}', 'set-dev-work': 'Là où travaille son outil',
|
|
194
|
+
'set-dev-files': 'Fichiers', 'set-dev-has': 'Ce qu’il a', 'set-dev-things-1': '1 chose', 'set-dev-things-n': '{n} choses',
|
|
195
|
+
'set-dev-seen': '{ago} · appairé le {date}', 'set-dev-collapse': 'Replier', 'set-dev-expand': 'Déplier',
|
|
196
|
+
'set-dev-rename': 'Renommer', 'set-dev-save': 'Enregistrer', 'set-dev-name': 'Nom de cet appareil',
|
|
197
|
+
'set-dev-revoke': 'Révoquer l’appareil', 'set-dev-revoke-q': '{name} perd tout, maintenant', 'set-dev-revoke-yes': 'Révoquer',
|
|
198
|
+
'set-dev-more': '+ Donner plus', 'set-dev-add': '+ Ajouter', 'set-dev-added': 'Ajouté', 'set-dev-remove': 'Retirer',
|
|
199
|
+
'set-dev-not-shared': 'Non partagé ({n})', 'set-dev-share': 'Partager…', 'set-dev-chat-off': 'Chat coupé : l’appareil attend',
|
|
192
200
|
};
|
|
193
201
|
export const settingsWords = (lang) => (String(lang ?? '').toLowerCase().split('-')[0] === 'fr' ? { ...SETTINGS_WORDS_FR } : {});
|
package/web/app.js
CHANGED
|
@@ -6140,7 +6140,7 @@
|
|
|
6140
6140
|
newAsset: () => newAssetFromMap(),
|
|
6141
6141
|
autoSending: (conversation, name) => autoSending(conversation, name),
|
|
6142
6142
|
partsFor: (name) => partsFor(name),
|
|
6143
|
-
deviceCard: (id) => { const d = sharedDevices.find((x) => x.id === id); return d ?
|
|
6143
|
+
deviceCard: (id) => { const d = sharedDevices.find((x) => x.id === id); return d ? devicePanel(d, '', true) : null; },
|
|
6144
6144
|
/* The map says when it turns to its flows by itself (a box's "Trace it"). */
|
|
6145
6145
|
modeIs: (mode) => { if (mode !== mapMode) { mapMode = mode; paintTabs(); subStep(); } },
|
|
6146
6146
|
/* Its box rising from the bottom on a phone: over the screen, closed by Back. */
|
|
@@ -9253,13 +9253,35 @@
|
|
|
9253
9253
|
return box;
|
|
9254
9254
|
}
|
|
9255
9255
|
|
|
9256
|
+
/* THE DEVICE'S FILES (/c/, assets step 6): the owner's one list (kit AssetList), scoped to this device -- by type, its
|
|
9257
|
+
right on each row, no « + New », no « Give… » -- and the one chosen opened in the asset panel (AssetPanel), its
|
|
9258
|
+
files inside it, read-only unless given to write. On a phone, the list or the one opened (« ‹ Files » back to the
|
|
9259
|
+
list); wider, both side by side. One thing only: opened at once. */
|
|
9260
|
+
let filesListing = false;
|
|
9261
|
+
function paintDeviceAssets(items) {
|
|
9262
|
+
const sheet = $('files');
|
|
9263
|
+
const model = KIT.assetListModel({ items, scope: `device:${pairing}`, kinds: assetKinds() });
|
|
9264
|
+
const chosen = items.find((i) => i.name === filesOf) ?? null;
|
|
9265
|
+
sheet.dataset.open = chosen && (!filesListing || items.length <= 1) ? 'detail' : 'list';
|
|
9266
|
+
sheet.dataset.many = String(items.length > 1);
|
|
9267
|
+
paintAssetList($('files-assets'), model, { chosen: chosen?.key ?? '',
|
|
9268
|
+
onOpen: (key) => { const i = items.find((x) => x.key === key); if (!i) return; filesListing = false; if (filesOf !== i.name) { filesOf = i.name; filesAt = ''; } void paintFiles(); } });
|
|
9269
|
+
const holder = $('files-panel');
|
|
9270
|
+
if (!chosen) { holder.replaceChildren(); holder.append($('files-body')); $('files-body').hidden = true; return; }
|
|
9271
|
+
$('files-body').hidden = false;
|
|
9272
|
+
const panel = KIT.AssetPanel(chosen, { back: items.length > 1 ? { label: `‹ ${uiSaid('set-dev-files', 'Files')}`, onBack: () => { filesListing = true; sheet.dataset.open = 'list'; } } : null,
|
|
9273
|
+
parts: [{ title: uiSaid('set-dev-files', 'Files'), body: [$('files-body')] }], cls: 'files-asset' });
|
|
9274
|
+
holder.replaceChildren(panel);
|
|
9275
|
+
}
|
|
9256
9276
|
/* `keep`: an error just said stays (painted after a change that failed in part). */
|
|
9257
9277
|
async function paintFiles({ keep = false } = {}) {
|
|
9258
9278
|
const ownerOthers = DEVICE ? [] : (await (await fetch(`${API}/resources`)).json().catch(() => [])).map((r) => ({ name: r.name, access: r.type === 'mailbox' ? 'read' : 'write', yours: true }));
|
|
9259
9279
|
/* First, where the tool works -- when that is kept here; then what is attached. */
|
|
9260
9280
|
const here = conversations.find((x) => x.name === current);
|
|
9261
9281
|
const attachedHere = here?.resources ?? [];
|
|
9262
|
-
|
|
9282
|
+
/* ON A DEVICE (/c/, assets step 6): what it sees is the kit's (deviceItems), drawn as the owner's one list. */
|
|
9283
|
+
const seen = DEVICE ? KIT.deviceItems({ shared: deviceShared, resources: attachedHere, work: here?.work, chatOnly: noTerminal(), conversation: shownName(here) || current }) : [];
|
|
9284
|
+
const mine = DEVICE ? seen.map((i) => ({ name: i.name, access: i.access, given: i.via === 'device', own: i.via === 'work' })) : [...(here?.work === 'kept' ? [{ name: 'work', access: 'write', own: true }] : []), ...attachedHere.map((r) => ({ ...r })),
|
|
9263
9285
|
/* On a device: what it was given for itself, whatever the conversation. */
|
|
9264
9286
|
...deviceShared.filter((s) => !attachedHere.some((r) => r.name === s.name)).map((s) => ({ name: s.name, access: s.access, given: true })),
|
|
9265
9287
|
/* For the owner: everything else that is kept here, attached to this conversation or not. */
|
|
@@ -9272,7 +9294,8 @@
|
|
|
9272
9294
|
if (!mine.some((r) => r.name === filesOf)) { filesOf = mine[0]?.name ?? ''; filesAt = ''; }
|
|
9273
9295
|
const tabs = $('files-tabs');
|
|
9274
9296
|
tabs.replaceChildren();
|
|
9275
|
-
|
|
9297
|
+
if (DEVICE) paintDeviceAssets(seen);
|
|
9298
|
+
for (const r of DEVICE ? [] : mine) {
|
|
9276
9299
|
const b = document.createElement('button');
|
|
9277
9300
|
b.type = 'button'; b.textContent = r.own ? 'work - where the tool works' : `${r.name} - ${r.access === 'write' ? 'read-write' : 'read-only'}${r.given ? ' (given to this device)' : r.yours ? ' (not attached here)' : ''}`; b.setAttribute('role', 'tab');
|
|
9278
9301
|
b.setAttribute('aria-selected', r.name === filesOf ? 'true' : 'false');
|
|
@@ -10589,8 +10612,9 @@
|
|
|
10589
10612
|
|
|
10590
10613
|
/* FILES, a sheet over the screen: on a device's page only (the owner's are in the Assets tab of a conversation). */
|
|
10591
10614
|
function showFiles() { if ($('assets-detail').contains($('files'))) unembedFiles(); nav.over('files', () => { if (!$('files').classList.contains('embedded')) { $('files').hidden = true; focusTerminal(); } }); $('files').hidden = false; }
|
|
10592
|
-
|
|
10593
|
-
$('files-
|
|
10615
|
+
/* On a device, Files opens on its list (a phone): the one to open is chosen there. */
|
|
10616
|
+
$('files-open').addEventListener('click', async () => { filesListing = DEVICE; await paintFiles(); showFiles(); });
|
|
10617
|
+
$('files-close').addEventListener('click', () => { filesListing = false; if (!nav.dropOver('files')) { $('files').hidden = true; focusTerminal(); } });
|
|
10594
10618
|
|
|
10595
10619
|
/* ---------- sign-in mailbox ---------- */
|
|
10596
10620
|
/* While a conversation with a mailbox is open, the page asks every few
|
|
@@ -10666,56 +10690,35 @@
|
|
|
10666
10690
|
$('sharing-top').hidden = which === 'manage';
|
|
10667
10691
|
};
|
|
10668
10692
|
|
|
10669
|
-
/* A
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
for (const [value, text] of [['r', 'Read'], ['rw', 'Write']]) {
|
|
10673
|
-
const b = document.createElement('button'); b.type = 'button'; b.textContent = text; b.dataset.rights = value;
|
|
10674
|
-
b.setAttribute('aria-pressed', String(value === rights));
|
|
10675
|
-
b.addEventListener('click', () => { if (value !== rights) onChange(value); });
|
|
10676
|
-
group.append(b);
|
|
10677
|
-
}
|
|
10678
|
-
return group;
|
|
10679
|
-
}
|
|
10680
|
-
const fixedRight = (type) => { const s = document.createElement('span'); s.className = 'hint'; s.textContent = type === 'conversation' ? 'Use - the whole terminal' : 'Read only'; return s; };
|
|
10681
|
-
/* A CONVERSATION IS GIVEN IN TWO PARTS, ticked one by one: its terminal
|
|
10682
|
-
("x") and its chat ("rw": read, and write there). At least one: the
|
|
10683
|
-
last one ticked cannot be unticked (taking it all back is "Take back").
|
|
10684
|
-
The server holds the device to it (see server/door.mjs). */
|
|
10693
|
+
/* A DEVICE'S RIGHT ON ONE THING (07-11, assets step 6): the kit's RightsPicker, its device column -- a folder, a
|
|
10694
|
+
repository: Read, Write; a mailbox: Read; a conversation in parts: Terminal, Chat, Both. « None » takes it back
|
|
10695
|
+
where it is given; in the share basket it is not offered (« Remove » is). Was levelControl, partsControl, fixedRight. */
|
|
10685
10696
|
const talksHere = (name) => Boolean(conversations.find((c) => c.name === name)?.talk);
|
|
10686
10697
|
/* What is proposed when a conversation is added: its chat where it talks through the app, else its terminal. */
|
|
10687
10698
|
const partsFor = (name) => (talksHere(name) ? 'rw' : 'x');
|
|
10688
|
-
function
|
|
10689
|
-
const
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
if (has.chat && !talksHere(name)) {
|
|
10711
|
-
const off = document.createElement('small'); off.className = 'hint parts-off'; off.textContent = 'Its chat is off: the device waits until you turn it on (General).'; off.title = 'Given, and waiting: the device sees its chat once you turn it on, in This conversation, then General.';
|
|
10712
|
-
group.append(off);
|
|
10713
|
-
}
|
|
10714
|
-
return group;
|
|
10699
|
+
function deviceRight(type, name, rights, onChange, { who = '', none = true } = {}) {
|
|
10700
|
+
const levels = KIT.rightsFor(type, 'device').filter((l) => none || l !== 'none');
|
|
10701
|
+
return KIT.RightsPicker({ key: type, holder: 'device', value: rights, levels, label: uiSaid('set-acc-what', 'What {who} may do with {name}', { who: who || 'it', name }), onChange });
|
|
10702
|
+
}
|
|
10703
|
+
/* Its chat given while it is off: said, so that nobody wonders why the device shows nothing. */
|
|
10704
|
+
function chatOff(type, name, rights) {
|
|
10705
|
+
if (type !== 'conversation' || !String(rights ?? '').includes('r') || talksHere(name)) return null;
|
|
10706
|
+
const off = el('small', 'hint parts-off', uiSaid('set-dev-chat-off', 'Chat off: the device waits'));
|
|
10707
|
+
off.title = 'Given, and waiting: the device sees its chat once you turn it on, in This conversation, then General.';
|
|
10708
|
+
return off;
|
|
10709
|
+
}
|
|
10710
|
+
/* One thing given, or to give, to a device: the asset it is (an AssetCard row), its right and gestures on its right. */
|
|
10711
|
+
function deviceThing(asset, { sub, actions = [], cls = '' } = {}) {
|
|
10712
|
+
const at = asset.indexOf(':'); const type = asset.slice(0, at); const name = asset.slice(at + 1);
|
|
10713
|
+
const row = KIT.AssetCard({ key: asset, kind: type, name, sub: sub ?? KIND_WORD[type] ?? type }, { tag: 'div', cls: `given-row dev-thing${cls ? ` ${cls}` : ''}`, actions });
|
|
10714
|
+
row.dataset.asset = asset;
|
|
10715
|
+
return row;
|
|
10716
|
+
}
|
|
10717
|
+
/* A right set from any of these places: given at its level, or taken back (« None »). */
|
|
10718
|
+
async function setDeviceRight(id, asset, level, stored) {
|
|
10719
|
+
if (level === 'none') await post(`${API}/devices/${id}/take`, { asset }).catch(() => {});
|
|
10720
|
+
else await post(`${API}/devices/${id}/give`, { asset, rights: stored }).catch(() => {});
|
|
10715
10721
|
}
|
|
10716
|
-
/* The right control of a thing given: a conversation in parts, a folder or a repository to read or write. */
|
|
10717
|
-
const rightControl = (type, name, rights, onChange, label) => (type === 'conversation' ? partsControl(rights, onChange, name)
|
|
10718
|
-
: type === 'folder' || type === 'git' ? levelControl(rights, onChange, label) : fixedRight(type));
|
|
10719
10722
|
|
|
10720
10723
|
async function loadCatalog() {
|
|
10721
10724
|
const things = await (await fetch(`${API}/resources`)).json().catch(() => []);
|
|
@@ -10763,13 +10766,12 @@
|
|
|
10763
10766
|
const box = $('share-basket'); box.replaceChildren();
|
|
10764
10767
|
for (const [asset, rights] of basket) {
|
|
10765
10768
|
const [type, name] = asset.split(':');
|
|
10766
|
-
const
|
|
10767
|
-
const label = document.createElement('b'); label.textContent = name;
|
|
10768
|
-
const kind = document.createElement('small'); kind.className = 'hint'; kind.textContent = ` ${KIND_WORD[type] ?? type}`; label.append(kind);
|
|
10769
|
-
const right = rightControl(type, name, rights, (value) => { basket.set(asset, value); paintCompose(); }, `What it may do with ${name}`);
|
|
10770
|
-
const out = document.createElement('button'); out.type = 'button'; out.textContent = 'Remove'; out.setAttribute('aria-label', `Remove ${name} from what it will get`);
|
|
10769
|
+
const out = el('button', '', uiSaid('set-dev-remove', 'Remove')); out.type = 'button'; out.setAttribute('aria-label', `Remove ${name} from what it will get`);
|
|
10771
10770
|
out.addEventListener('click', () => { basket.delete(asset); paintCompose(); });
|
|
10772
|
-
|
|
10771
|
+
const right = deviceRight(type, name, rights, (level, stored) => { basket.set(asset, stored); paintCompose(); }, { none: false });
|
|
10772
|
+
const row = deviceThing(asset, { actions: [right, out], cls: 'folder' });
|
|
10773
|
+
const off = chatOff(type, name, rights); if (off) row.append(off);
|
|
10774
|
+
box.append(row);
|
|
10773
10775
|
}
|
|
10774
10776
|
$('share-basket-title').textContent = `It will get (${basket.size})`;
|
|
10775
10777
|
$('share-basket-empty').hidden = basket.size > 0;
|
|
@@ -10787,17 +10789,14 @@
|
|
|
10787
10789
|
const order = (c) => `${c.near ? 0 : 1}${KINDS.findIndex(([k]) => k === c.type)}${c.name}`;
|
|
10788
10790
|
for (const c of [...catalog].sort((a, b2) => order(a).localeCompare(order(b2)))) {
|
|
10789
10791
|
if ((shareKind && c.type !== shareKind) || (wanted && !c.name.includes(wanted))) continue;
|
|
10790
|
-
const
|
|
10791
|
-
|
|
10792
|
-
const kind = document.createElement('small'); kind.className = 'hint'; kind.textContent = ` ${KIND_WORD[c.type]}${c.near && c.type !== 'conversation' ? ', attached here' : ''}`; label.append(kind);
|
|
10793
|
-
const plus = document.createElement('button'); plus.type = 'button';
|
|
10794
|
-
if (basket.has(c.asset)) { plus.textContent = 'Added'; plus.disabled = true; }
|
|
10792
|
+
const plus = el('button'); plus.type = 'button';
|
|
10793
|
+
if (basket.has(c.asset)) { plus.textContent = uiSaid('set-dev-added', 'Added'); plus.disabled = true; }
|
|
10795
10794
|
else {
|
|
10796
|
-
plus.textContent = '+ Add'; plus.setAttribute('aria-label', `Add ${c.name}`);
|
|
10795
|
+
plus.textContent = uiSaid('set-dev-add', '+ Add'); plus.setAttribute('aria-label', `Add ${c.name}`);
|
|
10797
10796
|
/* A conversation is given whole; anything else to read -- writing is asked for, never assumed. */
|
|
10798
10797
|
plus.addEventListener('click', () => { basket.set(c.asset, c.type === 'conversation' ? partsFor(c.name) : 'r'); paintCompose(); });
|
|
10799
10798
|
}
|
|
10800
|
-
|
|
10799
|
+
add.append(deviceThing(c.asset, { sub: `${KIND_WORD[c.type]}${c.near && c.type !== 'conversation' ? ', attached here' : ''}`, actions: [plus], cls: 'folder' }));
|
|
10801
10800
|
}
|
|
10802
10801
|
sayChoice();
|
|
10803
10802
|
}
|
|
@@ -10848,77 +10847,67 @@
|
|
|
10848
10847
|
const ask = document.createElement('span'); ask.className = 'callout bad confirm'; ask.setAttribute('role', 'alertdialog'); ask.append(keep, sure);
|
|
10849
10848
|
button.replaceWith(ask); sure.focus();
|
|
10850
10849
|
}
|
|
10851
|
-
|
|
10850
|
+
/* One thing a device has, its right set in place; « None » takes it back. */
|
|
10851
|
+
function deviceGrantRow(device, g) {
|
|
10852
10852
|
const [type, name] = g.asset.split(':');
|
|
10853
|
-
const
|
|
10854
|
-
const
|
|
10855
|
-
const
|
|
10856
|
-
const right = rightControl(type, name, g.rights, async (value) => { await post(`${API}/devices/${device.id}/give`, { asset: g.asset, rights: value }).catch(() => {}); await repaintDevices(); }, `What ${device.label} may do with ${name}`);
|
|
10857
|
-
const take = document.createElement('button'); take.type = 'button'; take.textContent = 'Take back';
|
|
10858
|
-
take.addEventListener('click', () => takeBack(take, `Take ${name} back from ${device.label}`, () => post(`${API}/devices/${device.id}/take`, { asset: g.asset })));
|
|
10859
|
-
row.append(label, right, take);
|
|
10853
|
+
const right = deviceRight(type, name, g.rights, async (level, stored) => { await setDeviceRight(device.id, g.asset, level, stored); await repaintDevices(); if (level === 'none') void refresh(); }, { who: device.label });
|
|
10854
|
+
const row = deviceThing(g.asset, { actions: [right] });
|
|
10855
|
+
const off = chatOff(type, name, g.rights); if (off) row.append(off);
|
|
10860
10856
|
return row;
|
|
10861
10857
|
}
|
|
10862
10858
|
let manageBy = 'device';
|
|
10863
|
-
const deviceShut = new Set(); /* the device
|
|
10859
|
+
const deviceShut = new Set(); /* the device panels folded on this page */
|
|
10864
10860
|
/* After a change to a device: its list (the Devices pane) or the map, drawn again. */
|
|
10865
10861
|
async function repaintDevices() {
|
|
10866
10862
|
if (place === 'access' && accessTab === 'devices') { await paintManage(); return; }
|
|
10867
10863
|
await readDevices(); await links?.changed();
|
|
10868
10864
|
}
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
const rename = document.createElement('button'); rename.type = 'button'; rename.textContent = 'Rename';
|
|
10875
|
-
rename.addEventListener('click', () => {
|
|
10876
|
-
const field = document.createElement('input'); field.value = device.label; field.maxLength = 40; field.setAttribute('aria-label', 'Name of this device');
|
|
10877
|
-
const save = document.createElement('button'); save.type = 'button'; save.className = 'primary'; save.textContent = 'Save';
|
|
10878
|
-
const done = async () => { if (field.value.trim()) await post(`${API}/devices/${device.id}/rename`, { label: field.value.trim() }).catch(() => {}); await repaintDevices(); };
|
|
10879
|
-
save.addEventListener('click', () => { void done(); });
|
|
10880
|
-
field.addEventListener('keydown', (event) => { if (event.key === 'Enter') void done(); event.stopPropagation(); });
|
|
10881
|
-
name.replaceWith(field); rename.replaceWith(save); focusField(field); if (document.activeElement === field) field.select();
|
|
10882
|
-
});
|
|
10883
|
-
const seen = document.createElement('small'); seen.textContent = `${ago(device.seenAt)} · paired ${new Date(device.createdAt).toLocaleDateString()}`;
|
|
10884
|
-
if (!device.seenAt) seen.className = 'warn';
|
|
10885
|
-
const revoke = document.createElement('button'); revoke.type = 'button'; revoke.textContent = 'Revoke device';
|
|
10886
|
-
revoke.addEventListener('click', () => takeBack(revoke, `Revoke ${device.label}: it loses all ${device.grants.length} thing${device.grants.length > 1 ? 's' : ''}, now`, () => post(`${API}/devices/${device.id}/revoke`)));
|
|
10887
|
-
/* A card that folds, as the chapters of a conversation do: open unless folded on purpose. */
|
|
10865
|
+
const thingsSaid = (n) => (n === 1 ? uiSaid('set-dev-things-1', '1 thing') : uiSaid('set-dev-things-n', '{n} things', { n }));
|
|
10866
|
+
/* ONE DEVICE OPENED (07-11, assets step 6): the asset panel -- its head (its name, how many things, when it was
|
|
10867
|
+
last seen; folded here), what it has (each thing with its right, « None » takes it back), then + Give more,
|
|
10868
|
+
Rename, Revoke device (asked in place). On the map's card of the device too (`onMap`: never folded). */
|
|
10869
|
+
function devicePanel(device, only, onMap = false) {
|
|
10888
10870
|
const shut = !onMap && deviceShut.has(device.id);
|
|
10889
|
-
const fold =
|
|
10890
|
-
fold.setAttribute('aria-expanded', String(!shut)); fold.setAttribute('aria-label', `${
|
|
10871
|
+
const fold = el('button', 'card-fold', shut ? uiSaid('set-dev-expand', 'Expand') : uiSaid('set-dev-collapse', 'Collapse')); fold.type = 'button';
|
|
10872
|
+
fold.setAttribute('aria-expanded', String(!shut)); fold.setAttribute('aria-label', `${fold.textContent} ${device.label}`);
|
|
10891
10873
|
fold.addEventListener('click', () => { if (shut) deviceShut.delete(device.id); else deviceShut.add(device.id); void repaintDevices(); });
|
|
10892
|
-
const
|
|
10893
|
-
const
|
|
10894
|
-
const head = document.createElement('div'); head.className = 'device-head'; head.append(name, sub, fold);
|
|
10895
|
-
const given = document.createElement('div'); given.className = 'given';
|
|
10874
|
+
const seen = uiSaid('set-dev-seen', '{ago} · paired {date}', { ago: ago(device.seenAt), date: new Date(device.createdAt).toLocaleDateString() });
|
|
10875
|
+
const given = el('div', 'given');
|
|
10896
10876
|
const mine = only ? device.grants.filter((g) => g.asset === only) : device.grants;
|
|
10897
|
-
for (const g of mine) given.append(
|
|
10898
|
-
/* Several chats given while off: said once for the
|
|
10877
|
+
for (const g of mine) given.append(deviceGrantRow(device, g));
|
|
10878
|
+
/* Several chats given while off: said once for the panel, not under each. */
|
|
10899
10879
|
const waiting = [...given.querySelectorAll('.parts-off')];
|
|
10900
10880
|
if (waiting.length > 1) {
|
|
10901
10881
|
for (const w of waiting) w.remove();
|
|
10902
|
-
const once =
|
|
10903
|
-
once.textContent = `${waiting.length} chats are off: the device waits until you turn each on (the conversation's General).`;
|
|
10882
|
+
const once = el('p', 'hint parts-off', `${waiting.length} chats are off: the device waits until you turn each on (the conversation's General).`); once.title = waiting[0].title;
|
|
10904
10883
|
given.prepend(once);
|
|
10905
10884
|
}
|
|
10906
10885
|
if (only && mine.length === 0) {
|
|
10907
|
-
const
|
|
10908
|
-
const label = document.createElement('span'); label.textContent = accessFor;
|
|
10909
|
-
const none = document.createElement('small'); none.className = 'hint'; none.textContent = ' not shared'; label.append(none);
|
|
10910
|
-
const give = document.createElement('button'); give.type = 'button'; give.className = 'primary'; give.textContent = `Give ${accessFor}`;
|
|
10886
|
+
const give = el('button', 'primary', `Give ${accessFor}`); give.type = 'button';
|
|
10911
10887
|
give.addEventListener('click', async () => { give.disabled = true; await post(`${API}/devices/${device.id}/give`, { asset: only, rights: partsFor(accessFor) }).catch(() => {}); await repaintDevices(); void refresh(); });
|
|
10912
|
-
|
|
10888
|
+
given.append(deviceThing(only, { sub: 'not shared', actions: [give] }));
|
|
10913
10889
|
}
|
|
10914
10890
|
/* What the filter leaves out is named, never only counted. */
|
|
10915
|
-
if (only && device.grants.length > mine.length)
|
|
10916
|
-
const more =
|
|
10891
|
+
if (only && device.grants.length > mine.length) given.append(el('p', 'hint', `It also has: ${device.grants.filter((g) => g.asset !== only).map((g) => { const [type, what] = g.asset.split(':'); return `${what} (${KIND_WORD[type] ?? type})`; }).join(', ')}.`));
|
|
10892
|
+
const more = el('button', '', uiSaid('set-dev-more', '+ Give more')); more.type = 'button';
|
|
10917
10893
|
more.addEventListener('click', () => { void openCompose([], device.id); });
|
|
10918
|
-
const
|
|
10919
|
-
const
|
|
10920
|
-
|
|
10921
|
-
|
|
10894
|
+
const rename = el('button', '', uiSaid('set-dev-rename', 'Rename')); rename.type = 'button';
|
|
10895
|
+
const revoke = KIT.AssetDelete({ label: uiSaid('set-dev-revoke', 'Revoke device'), lost: uiSaid('set-dev-revoke-q', '{name} loses everything, now', { name: device.label }), yes: uiSaid('set-dev-revoke-yes', 'Revoke'),
|
|
10896
|
+
onYes: async () => { await post(`${API}/devices/${device.id}/revoke`).catch(() => {}); await repaintDevices(); void refresh(); } });
|
|
10897
|
+
const panel = KIT.AssetPanel({ key: `device:${device.id}`, kind: 'device', name: device.label, sub: `${thingsSaid(device.grants.length)} · ${seen}`, tone: device.seenAt ? '' : 'warn' },
|
|
10898
|
+
{ cls: 'device-panel', actions: onMap ? [] : [fold], parts: [{ title: uiSaid('set-dev-has', 'What it has'), body: [given] }], foot: [more, rename, revoke] });
|
|
10899
|
+
panel.dataset.device = device.id;
|
|
10900
|
+
if (shut) for (const part of panel.children) if (!part.classList.contains('as-title')) part.hidden = true;
|
|
10901
|
+
rename.addEventListener('click', () => {
|
|
10902
|
+
const name = panel.querySelector('.kit-asset-name');
|
|
10903
|
+
const field = document.createElement('input'); field.value = device.label; field.maxLength = 40; field.setAttribute('aria-label', uiSaid('set-dev-name', 'Name of this device'));
|
|
10904
|
+
const save = el('button', 'primary', uiSaid('set-dev-save', 'Save')); save.type = 'button';
|
|
10905
|
+
const done = async () => { if (field.value.trim()) await post(`${API}/devices/${device.id}/rename`, { label: field.value.trim() }).catch(() => {}); await repaintDevices(); };
|
|
10906
|
+
save.addEventListener('click', () => { void done(); });
|
|
10907
|
+
field.addEventListener('keydown', (event) => { if (event.key === 'Enter') void done(); event.stopPropagation(); });
|
|
10908
|
+
name?.replaceWith(field); rename.replaceWith(save); focusField(field); if (document.activeElement === field) field.select();
|
|
10909
|
+
});
|
|
10910
|
+
return panel;
|
|
10922
10911
|
}
|
|
10923
10912
|
/* ---- voice boxes (boxes.mjs): pairing codes, the flash from this browser, the list ---- */
|
|
10924
10913
|
/* THE WAKE WORD THAT LEARNS (wakelearn.mjs): normal, or each wake clip kept with its label to train a better one. */
|
|
@@ -11265,34 +11254,32 @@
|
|
|
11265
11254
|
if (manageBy === 'device' || only) {
|
|
11266
11255
|
rows.hidden = false; things.hidden = true;
|
|
11267
11256
|
for (const device of [...sharedDevices].sort((a, b2) => String(b2.seenAt ?? '').localeCompare(String(a.seenAt ?? '')))) {
|
|
11268
|
-
rows.append(
|
|
11257
|
+
rows.append(devicePanel(device, only));
|
|
11269
11258
|
}
|
|
11270
11259
|
return;
|
|
11271
11260
|
}
|
|
11272
|
-
/* By thing: who can see this folder?
|
|
11261
|
+
/* By thing: who can see this folder? Each thing (an AssetCard), who has it (the kit's AccessList, devices only:
|
|
11262
|
+
each with its right, « None » takes it back). What nobody has is folded away, each with « Share… ». */
|
|
11273
11263
|
await loadCatalog();
|
|
11274
11264
|
const alone = [];
|
|
11275
11265
|
for (const c of catalog) {
|
|
11276
11266
|
const holders = devicesWith(c.asset);
|
|
11277
11267
|
if (holders.length === 0) { alone.push(c); continue; }
|
|
11278
|
-
const block =
|
|
11279
|
-
const top =
|
|
11268
|
+
const block = el('details', 'thing'); block.dataset.asset = c.asset; block.open = true;
|
|
11269
|
+
const top = el('summary');
|
|
11270
|
+
top.append(KIT.AssetCard({ key: c.asset, kind: c.type, name: c.name, sub: `${KIND_WORD[c.type]} · ${holders.length === 1 ? uiSaid('set-device', '1 device') : uiSaid('set-devices', '{n} devices', { n: holders.length })}` }, { tag: 'span' }));
|
|
11280
11271
|
block.append(top);
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
row.
|
|
11284
|
-
block.append(row);
|
|
11285
|
-
}
|
|
11272
|
+
const model = KIT.accessModel({ key: c.type, holders: holders.map((d) => ({ id: `device:${d.id}`, name: d.label, sub: ago(d.seenAt), value: d.grants.find((g) => g.asset === c.asset).rights })) });
|
|
11273
|
+
block.append(KIT.AccessList(model, { key: c.type, name: c.name, title: '', give: false,
|
|
11274
|
+
onChange: async (row, level, stored) => { await setDeviceRight(row.id.slice('device:'.length), c.asset, level, stored); await repaintDevices(); if (level === 'none') void refresh(); } }));
|
|
11286
11275
|
things.append(block);
|
|
11287
11276
|
}
|
|
11288
|
-
const rest =
|
|
11289
|
-
|
|
11277
|
+
const rest = el('details', 'thing');
|
|
11278
|
+
rest.append(el('summary', '', uiSaid('set-dev-not-shared', 'Not shared ({n})', { n: alone.length })));
|
|
11290
11279
|
for (const c of alone) {
|
|
11291
|
-
const
|
|
11292
|
-
const label = document.createElement('span'); label.textContent = `${c.name} - ${KIND_WORD[c.type]}`;
|
|
11293
|
-
const give = document.createElement('button'); give.type = 'button'; give.textContent = 'Share...';
|
|
11280
|
+
const give = el('button', '', uiSaid('set-dev-share', 'Share…')); give.type = 'button';
|
|
11294
11281
|
give.addEventListener('click', () => { void openCompose([[c.asset, c.type === 'conversation' ? partsFor(c.name) : 'r']]); });
|
|
11295
|
-
|
|
11282
|
+
rest.append(deviceThing(c.asset, { actions: [give] }));
|
|
11296
11283
|
}
|
|
11297
11284
|
things.append(rest);
|
|
11298
11285
|
}
|
|
@@ -11316,23 +11303,15 @@
|
|
|
11316
11303
|
await readDevices();
|
|
11317
11304
|
const box = $('share-here'); box.replaceChildren();
|
|
11318
11305
|
const asset = `conversation:${current}`;
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
const
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
keep.addEventListener('click', () => { void paintShareHere(); });
|
|
11329
|
-
sure.addEventListener('click', async () => { await post(`${API}/devices/${device.id}/take`, { asset }).catch(() => {}); await paintShareHere(); void refresh(); });
|
|
11330
|
-
const ask = document.createElement('span'); ask.className = 'callout bad confirm'; ask.setAttribute('role', 'alertdialog'); ask.append(keep, sure);
|
|
11331
|
-
take.replaceWith(ask); sure.focus();
|
|
11332
|
-
});
|
|
11333
|
-
row.append(label, right, take); box.append(row);
|
|
11334
|
-
}
|
|
11335
|
-
$('share-here-none').hidden = box.children.length > 0;
|
|
11306
|
+
/* The devices that have it (the kit's AccessList): each with its parts, « None » takes it back. */
|
|
11307
|
+
const holders = devicesWith(asset).map((d) => ({ id: `device:${d.id}`, name: d.label, sub: ago(d.seenAt), value: d.grants.find((x) => x.asset === asset).rights }));
|
|
11308
|
+
if (holders.length) {
|
|
11309
|
+
box.append(KIT.AccessList(KIT.accessModel({ key: 'conversation', holders }), { key: 'conversation', name: current, title: '', give: false,
|
|
11310
|
+
onChange: async (row, level, stored) => { await setDeviceRight(row.id.slice('device:'.length), asset, level, stored); await paintShareHere(); if (level === 'none') void refresh(); } }));
|
|
11311
|
+
const off = chatOff('conversation', current, holders.find((h) => String(h.value).includes('r'))?.value);
|
|
11312
|
+
if (off) box.append(off);
|
|
11313
|
+
}
|
|
11314
|
+
$('share-here-none').hidden = holders.length > 0;
|
|
11336
11315
|
}
|
|
11337
11316
|
/* Back from choosing or from the code: where it was opened from, as Back and Escape. */
|
|
11338
11317
|
const leaveCompose = () => { void refresh(); if (!nav.close('compose')) leavePlace(); };
|
package/web/device.html
CHANGED
|
@@ -62,8 +62,16 @@
|
|
|
62
62
|
</div>
|
|
63
63
|
<div id="files" class="sheet" hidden>
|
|
64
64
|
<div class="panel-top"><b>Files</b><button type="button" id="files-close" aria-label="Close" title="Close">×</button></div>
|
|
65
|
-
<div id="files-tabs" class="choices" role="tablist" aria-label="Attached folders"></div>
|
|
66
|
-
|
|
65
|
+
<div id="files-tabs" class="choices" role="tablist" aria-label="Attached folders" hidden></div>
|
|
66
|
+
<!-- WHAT THIS DEVICE SEES (assets step 6): the owner's one asset list, scoped to the device, read by its rights;
|
|
67
|
+
one opened in the same asset panel, its files inside it. No New, no Give. -->
|
|
68
|
+
<div id="files-split">
|
|
69
|
+
<div id="files-side">
|
|
70
|
+
<div id="files-assets"></div>
|
|
71
|
+
<p id="files-none" class="hint" hidden>Nothing is shared with this device.</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="files-panel">
|
|
74
|
+
<div id="files-body">
|
|
67
75
|
<div class="files-bar">
|
|
68
76
|
<div id="files-path" class="path"></div>
|
|
69
77
|
<div id="files-views" class="levels" role="group" aria-label="Show as">
|
|
@@ -100,6 +108,9 @@
|
|
|
100
108
|
</form>
|
|
101
109
|
<div id="files-rows" data-view="list"></div>
|
|
102
110
|
<p id="files-empty" class="hint" hidden>This folder is empty.</p>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
103
114
|
<div id="files-drop" hidden><p id="files-drop-text">Drop to upload here</p></div>
|
|
104
115
|
</div>
|
|
105
116
|
<!-- A file, shown over whatever is on the screen; closing it leaves you where you were. -->
|
package/web/kit.js
CHANGED
|
@@ -688,9 +688,12 @@
|
|
|
688
688
|
'': groups once one exists, else types
|
|
689
689
|
query a name searched (any case) folded group ids folded shut kinds [[kind, title]] in order
|
|
690
690
|
-> { view, canGroup, sections: [{ id, title, kind | group, items, holders, folded, drop, empty }], shown, total } */
|
|
691
|
-
|
|
691
|
+
/* 'device:<pairing>' (07-11, assets step 6): what a device sees on /c/ -- read by its rights, nothing to make or give. */
|
|
692
|
+
const scopeOf = (scope) => { const s = String(scope ?? 'all'); return s.startsWith('conversation:') ? 'conversation' : s.startsWith('device:') ? 'device' : 'all'; };
|
|
692
693
|
function assetListModel({ items = [], groups = [], scope = 'all', by = '', query = '', folded = [], kinds = [] } = {}) {
|
|
693
694
|
const canGroup = scopeOf(scope) === 'all';
|
|
695
|
+
/* « + New » and « Give… »: the owner's (the menu, a conversation's tab), never a device's. */
|
|
696
|
+
const canNew = scopeOf(scope) !== 'device'; const canGive = canNew;
|
|
694
697
|
const view = canGroup && (by === 'group' || (by !== 'type' && groups.length > 0)) ? 'group' : 'type';
|
|
695
698
|
const q = String(query ?? '').trim().toLowerCase();
|
|
696
699
|
const shown = q ? items.filter((i) => String(i.name ?? '').toLowerCase().includes(q)) : [...items];
|
|
@@ -720,7 +723,33 @@
|
|
|
720
723
|
/* The loose ones stay a place to drag to (out of a group) while there are groups, even emptied. */
|
|
721
724
|
if (loose.length || (!q && items.length)) sections.push({ id: 'group:', group: '', title: say('set-as-no-group', 'No group'), items: loose, drop: '', empty: loose.length ? '' : groups.length ? 'grouped' : 'nogroups' });
|
|
722
725
|
}
|
|
723
|
-
return { view, canGroup, sections, shown: shown.length, total: items.length };
|
|
726
|
+
return { view, canGroup, canNew, canGive, scope: scopeOf(scope), sections, shown: shown.length, total: items.length };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/* WHAT A DEVICE SEES (07-11, assets step 6), as the list's items: its conversation's work folder (kept here) and what
|
|
730
|
+
is attached to it, then what was given to the device itself; given its chat alone, only the latter. The larger of
|
|
731
|
+
two rights holds (given to write, attached to read). Each with its right as the device has it (a badge, read only).
|
|
732
|
+
shared [{ name, type, access }] given to the device (GET /c/api/<pairing>/me, `shared`)
|
|
733
|
+
resources [{ name, access, type }] attached to its conversation work 'kept' | 'disk'
|
|
734
|
+
chatOnly given the chat alone conversation its name */
|
|
735
|
+
function deviceItems({ shared = [], resources = [], work = '', chatOnly = false, conversation = '' } = {}) {
|
|
736
|
+
const kind = (t) => (t === 'mailbox' || t === 'git' ? t : 'folder');
|
|
737
|
+
const out = new Map();
|
|
738
|
+
const put = (r, via) => {
|
|
739
|
+
if (!r?.name) return;
|
|
740
|
+
const access = r.access === 'write' ? 'write' : 'read';
|
|
741
|
+
const was = out.get(r.name);
|
|
742
|
+
if (was) { if (access === 'write') was.access = 'write'; if (via === 'device') was.via = 'device'; return; }
|
|
743
|
+
out.set(r.name, { name: r.name, kind: kind(r.type), access, via });
|
|
744
|
+
};
|
|
745
|
+
if (!chatOnly) {
|
|
746
|
+
if (work === 'kept') put({ name: 'work', access: 'write' }, 'work');
|
|
747
|
+
for (const r of resources ?? []) put(r, 'conversation');
|
|
748
|
+
}
|
|
749
|
+
for (const r of shared ?? []) put(r, 'device');
|
|
750
|
+
return [...out.values()].map((i) => ({ key: `${i.kind}:${i.name}`, kind: i.kind, name: i.name, access: i.access, via: i.via,
|
|
751
|
+
right: { key: i.kind, level: i.access, holder: 'device' },
|
|
752
|
+
sub: i.via === 'work' ? say('set-dev-work', 'Where its tool works') : i.via === 'device' ? say('set-dev-given', 'Given to this device') : say('set-dev-with', 'With {name}').replace('{name}', conversation) }));
|
|
724
753
|
}
|
|
725
754
|
|
|
726
755
|
/* The list drawn from its model. A row is an AssetCard (`rowCls` added: the page's handle on it); `pinned`,
|
|
@@ -729,6 +758,7 @@
|
|
|
729
758
|
when a search finds nothing. `after(a)`: a node drawn under a row (Give…: the chosen one's right). */
|
|
730
759
|
function AssetList(model, { chosen = '', onOpen = () => {}, onFold = () => {}, onGroup = () => {}, rowCls = '', pinned = [], empty = '', noMatch = '', after = null } = {}) {
|
|
731
760
|
const box = node('div', 'kit-aslist'); box.dataset.view = model.view;
|
|
761
|
+
if (model.scope) box.dataset.scope = model.scope;
|
|
732
762
|
box.append(...pinned.filter(Boolean));
|
|
733
763
|
const hints = {
|
|
734
764
|
group: say('set-as-group-empty', 'Empty: drag an asset here'),
|
|
@@ -854,17 +884,20 @@
|
|
|
854
884
|
/* The list drawn from its model. `onChange(row, level, stored)` sets one right (the picker waits on it); `kind`, the
|
|
855
885
|
asset's own kind (a calendar's levels say what they mean); `actions`, more gestures under it (Share with a device…).
|
|
856
886
|
Its old handles kept for the pages' tests: .as-chapter .as-add, .as-users, .as-picker, .given-row[data-conversation]. */
|
|
857
|
-
|
|
887
|
+
/* `title`: '' draws no head (the page has its own: a device's thing, a conversation's Sharing); `give`: false, no
|
|
888
|
+
« + Give to… » (a device is shared from its own sheet). */
|
|
889
|
+
function AccessList(model, { key = '', kind = '', name = '', levels = null, onChange = () => {}, actions = [], title, give: giving = true } = {}) {
|
|
858
890
|
const box = node('section', 'kit-access');
|
|
859
891
|
const head = node('div', 'as-chapter kit-access-head');
|
|
860
|
-
|
|
892
|
+
const heading = title ?? say('set-acc-title', 'Who has it');
|
|
893
|
+
if (heading) head.append(node('h3', '', heading));
|
|
861
894
|
const picker = node('div', 'as-users as-picker kit-access-pick'); picker.hidden = true;
|
|
862
895
|
const give = node('button', 'as-add kit-access-give', '+'); give.type = 'button';
|
|
863
896
|
const giveWord = say('set-acc-give', 'Give to…');
|
|
864
897
|
give.setAttribute('aria-label', giveWord); give.title = giveWord; give.setAttribute('aria-expanded', 'false');
|
|
865
898
|
give.addEventListener('click', () => { picker.hidden = !picker.hidden; give.setAttribute('aria-expanded', String(!picker.hidden)); });
|
|
866
|
-
head.append(give);
|
|
867
|
-
box.append(head);
|
|
899
|
+
if (giving) head.append(give);
|
|
900
|
+
if (head.children.length) box.append(head);
|
|
868
901
|
const row = (r) => {
|
|
869
902
|
const line = node('div', 'given-row kit-access-row');
|
|
870
903
|
line.dataset[r.holder === 'device' ? 'device' : 'conversation'] = String(r.id).replace(/^[a-z]+:/, '');
|
|
@@ -898,9 +931,11 @@
|
|
|
898
931
|
const said = { nobody: say('set-acc-nobody', 'No conversation uses it.'), everyone: '', noready: say('set-acc-noready', 'No conversation is ready.') }[model.said];
|
|
899
932
|
if (said && !model.here) users.append(node('p', 'hint', said));
|
|
900
933
|
if (users.children.length) box.append(users);
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
934
|
+
if (giving) {
|
|
935
|
+
for (const r of model.others) picker.append(row(r));
|
|
936
|
+
if (!model.others.length) picker.append(node('p', 'hint', model.said === 'noready' ? say('set-acc-noready', 'No conversation is ready.') : say('set-acc-everyone', 'Every conversation has it.')));
|
|
937
|
+
box.append(picker);
|
|
938
|
+
}
|
|
904
939
|
const more = actions.filter(Boolean);
|
|
905
940
|
if (more.length) { const acts = node('div', 'kit-access-acts'); acts.append(...more); box.append(acts); }
|
|
906
941
|
return box;
|
|
@@ -1013,7 +1048,8 @@
|
|
|
1013
1048
|
back { label?, onBack } the way back parts [node | { title, extra: [nodes], body: [nodes], cls }]
|
|
1014
1049
|
foot [nodes] before [nodes] above the head (an ending: "✓ created")
|
|
1015
1050
|
A part's title draws a chapter head (.as-chapter), its `extra` on its right. */
|
|
1016
|
-
|
|
1051
|
+
/* `actions`: gestures on its head's right (a device's fold). */
|
|
1052
|
+
function AssetPanel(card, { back = null, parts = [], foot = [], before = [], cls = '', actions = [] } = {}) {
|
|
1017
1053
|
const root = node('div', `kit-aspanel${cls ? ` ${cls}` : ''}`);
|
|
1018
1054
|
if (card?.key) root.dataset.asset = card.key;
|
|
1019
1055
|
root.append(...before.filter(Boolean));
|
|
@@ -1023,7 +1059,7 @@
|
|
|
1023
1059
|
b.addEventListener('click', () => back.onBack?.());
|
|
1024
1060
|
top.append(b);
|
|
1025
1061
|
}
|
|
1026
|
-
top.append(AssetCard(card ?? {}, { size: 'head' }));
|
|
1062
|
+
top.append(AssetCard(card ?? {}, { size: 'head', actions }));
|
|
1027
1063
|
root.append(top);
|
|
1028
1064
|
for (const p of parts) {
|
|
1029
1065
|
if (!p) continue;
|
|
@@ -1063,7 +1099,7 @@
|
|
|
1063
1099
|
|
|
1064
1100
|
window.clavariumKit = {
|
|
1065
1101
|
collectModel,
|
|
1066
|
-
AssetIcon, AssetCard, Holders, MemberStack, memberStackModel, roomPatch, RightsBadge, RightsPicker, AssetList, assetListModel, AccessList, accessModel, AssetPanel, AssetDelete, AssetPicker, assetPickModel, RIGHT_SAME,
|
|
1102
|
+
AssetIcon, AssetCard, Holders, MemberStack, memberStackModel, roomPatch, RightsBadge, RightsPicker, AssetList, assetListModel, deviceItems, AccessList, accessModel, AssetPanel, AssetDelete, AssetPicker, assetPickModel, RIGHT_SAME,
|
|
1067
1103
|
memberLevel, groupLevels, groupLevel, groupFanOut, skippedSaid, GroupChip,
|
|
1068
1104
|
ASSET_ICONS, ASSET_KINDS, RIGHTS, RIGHT_LEVELS, MCP_WORDS, rightsKey, rightsFor, rightOf, storedOf, lightest, rightWord,
|
|
1069
1105
|
shareSlices, ShareDonut, ShareKey,
|
package/web/style.css
CHANGED
|
@@ -910,6 +910,17 @@ svg.lk-links path.temp { stroke: var(--k-pick); stroke-dasharray: 4 4; }
|
|
|
910
910
|
.files-card b { overflow-wrap: anywhere; }
|
|
911
911
|
#files-ask, #files-progress { position: sticky; top: 0; z-index: 2; box-shadow: 0 6px 18px rgba(0, 0, 0, .25); }
|
|
912
912
|
#files-ask { border-color: var(--brass); }
|
|
913
|
+
/* A device's Files (/c/, assets step 6): the one asset list beside the asset opened; on a phone, one or the other. */
|
|
914
|
+
#files-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; min-width: 0; }
|
|
915
|
+
#files-side, #files-body { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
|
|
916
|
+
#files-panel { min-width: 0; }
|
|
917
|
+
#files-side .kit-aslist-head h3 { margin: 6px 0 2px; font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink2); }
|
|
918
|
+
#files[data-open="list"] #files-panel, #files[data-open="detail"] #files-side { display: none; }
|
|
919
|
+
@media (min-width: 720px) {
|
|
920
|
+
#files[data-many="true"] #files-split { grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); align-items: start; }
|
|
921
|
+
#files[data-many="true"] #files-side, #files[data-many="true"] #files-panel { display: flex; flex-direction: column; }
|
|
922
|
+
#files-panel .as-back { display: none; }
|
|
923
|
+
}
|
|
913
924
|
#files-name input { font: .875rem var(--sans); color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 7px; padding: 8px 10px; min-width: 0; width: min(100%, 420px); }
|
|
914
925
|
#files-name label { font-size: .75rem; color: var(--ink2); }
|
|
915
926
|
.bar { height: 8px; border-radius: 999px; background: var(--line); overflow: hidden; }
|
|
@@ -2937,3 +2948,29 @@ button.kit-switch[aria-checked="true"]::after { left: 18px; background: var(--ok
|
|
|
2937
2948
|
.tidy-wrap .lk-sheet { width: min(440px, 92vw); border: 1px solid var(--line); border-radius: 14px; padding: 16px; }
|
|
2938
2949
|
.tidy-wrap .lk-sheet::before { display: none; }
|
|
2939
2950
|
}
|
|
2951
|
+
|
|
2952
|
+
/* A DEVICE'S SHARING ON THE SAME PIECES (07-11, assets step 6): a device opened is the asset panel; each thing it has, or
|
|
2953
|
+
may get, an AssetCard row with its right (RightsPicker) on its right; by thing, the thing's row and who has it. */
|
|
2954
|
+
.device-panel { border: 1px solid var(--line); border-radius: 10px; background: var(--bar); padding: 10px 12px; margin-top: 8px; gap: 8px; }
|
|
2955
|
+
.device-panel .kit-aspanel-part .as-chapter h3 { margin: 0; font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink2); }
|
|
2956
|
+
.device-panel .given { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
|
|
2957
|
+
.device-panel .given > * { border-top: 1px solid var(--line); }
|
|
2958
|
+
.device-panel .given > :first-child { border-top: 0; }
|
|
2959
|
+
.device-panel .given > .hint { margin: 0; padding: 8px 12px; }
|
|
2960
|
+
.device-panel input { min-height: 40px; font: .875rem var(--sans); color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 7px; padding: 7px 9px; min-width: 0; flex: 1 1 auto; }
|
|
2961
|
+
.device-panel .kit-aspanel-foot { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
2962
|
+
.device-panel .kit-aspanel-foot button { min-height: 40px; }
|
|
2963
|
+
.kit-asset.dev-thing { flex-wrap: wrap; padding: 8px 12px; border-radius: 0; gap: 6px 10px; }
|
|
2964
|
+
.kit-asset.dev-thing .kit-asset-main { flex: 1 1 140px; }
|
|
2965
|
+
.kit-asset.dev-thing .kit-asset-side { flex: 0 1 auto; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }
|
|
2966
|
+
.kit-asset.dev-thing > .parts-off { flex-basis: 100%; }
|
|
2967
|
+
.kit-asset.dev-thing .kit-rights-pick button { min-height: 36px; }
|
|
2968
|
+
#share-basket, #share-add { display: flex; flex-direction: column; }
|
|
2969
|
+
#share-basket > .dev-thing, #share-add > .dev-thing { border-top: 1px solid var(--line); }
|
|
2970
|
+
.thing > summary .kit-asset { flex: 1 1 auto; padding: 4px 0; }
|
|
2971
|
+
.thing > .kit-access { padding: 0 12px 10px; }
|
|
2972
|
+
.thing > .kit-access .as-users { background: var(--panel); }
|
|
2973
|
+
.thing > .dev-thing { border-top: 1px solid var(--line); }
|
|
2974
|
+
#share-here .kit-access .given-row { padding: 10px 12px; }
|
|
2975
|
+
.kit-asset.dev-thing > .kit-asset-ic { flex: none; }
|
|
2976
|
+
.device-panel .kit-aspanel-foot.as-foot { flex-direction: row; justify-content: flex-start; align-items: center; }
|