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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ jobs:
- run: npm ci
- run: npm run build
# svelte-check exits non-zero at the legacy baseline; fail ONLY when the
# counts grow past it (baseline 435 errors / 62 warnings, 2026-08-01, node 24)
# counts grow past it (baseline 421 errors / 62 warnings, 2026-08-01 — the
# roadmap #15 C one-way `hex` picker rework dropped it from 435, node 24)
- name: svelte-check baseline gate
run: |
npm run check 2>&1 | tee check.log || true
Expand All @@ -34,9 +35,9 @@ jobs:
ERRORS=$(echo "$LINE" | awk '{print $2}')
WARNINGS=$(echo "$LINE" | awk '{print $5}')
fi
echo "svelte-check: $ERRORS errors / $WARNINGS warnings (baseline 435/62)"
echo "svelte-check: $ERRORS errors / $WARNINGS warnings (baseline 421/62)"
if [ -z "$ERRORS" ]; then echo "could not parse svelte-check output"; exit 1; fi
if [ "$ERRORS" -gt 435 ] || [ "$WARNINGS" -gt 62 ]; then
if [ "$ERRORS" -gt 421 ] || [ "$WARNINGS" -gt 62 ]; then
echo "baseline exceeded"; exit 1
fi
- name: zip the build
Expand Down
11 changes: 10 additions & 1 deletion src/components/editors/Explorer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -832,12 +832,21 @@
}

// N6: place a default-pack item into the scene (double-click / Enter) at origin
// 15-B3: the CDN fetch takes seconds — hold the SAME loading toast the drop
// path shows (it used to look like nothing happened until the model popped in)
async function placePackItem(item: any) {
const { holdLoadingToast } = await import('$lib/explorerDrop');
const dismiss = holdLoadingToast(String(item.name || 'model'));
try {
const res = await fetch(item.glbUrl);
if (!res.ok) return showToast('Could not fetch the pack item');
if (!res.ok) {
dismiss();
return showToast('Could not fetch the pack item');
}
await importFile(new File([await res.blob()], item.name + '.glb'), item.name, 'glb');
dismiss();
} catch {
dismiss();
showToast('Could not load the pack item (check the network / CORS)');
}
}
Expand Down
39 changes: 38 additions & 1 deletion src/components/menu/Connect.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { ChevronDown, Copy, Globe } from '@lucide/svelte';
import { peers, userdata, waitingForApproval, pendingApprovals, showToast, settingsOpen, settingsSection, connectDrawerOpen, connectDrawerTab, connectDrawerPinned, showRoomsButton, connectDocked, connectBarHeight } from '../../stores/appStore'
import { peers, userdata, waitingForApproval, pendingApprovals, showToast, settingsOpen, settingsSection, connectDrawerOpen, connectDrawerTab, connectDrawerPinned, showRoomsButton, connectDocked, connectBarHeight, toastStore, toastsInDrawerOnly } from '../../stores/appStore'
import { Input, Button } from 'flowbite-svelte';
import { onMount, tick } from 'svelte';
import { createPeer, PeerConnection } from '$lib/peerHandler.svelte';
Expand Down Expand Up @@ -44,6 +44,12 @@
);
// the drawer is visible when open OR pinned (pinned keeps the tab bar under the pill)
const drawerVisible = $derived($connectDrawerOpen || $connectDrawerPinned);
// 15-B4: toasts routed drawer-only are INVISIBLE while the drawer is closed —
// badge the chevron with the same count its Toasts tab shows (approvals ride
// along; they turn it amber, matching .cxd-tab-badge.req).
const hiddenToastCount = $derived(
!$connectDrawerOpen && $toastsInDrawerOnly ? $pendingApprovals.length + $toastStore.length : 0
);

// --- Responsive DOCKING (roadmap follow-up) --------------------------------
// The pill is centred at the top. On a wide screen there's room for it between
Expand Down Expand Up @@ -281,6 +287,13 @@
onclick={toggleInfo}
>
<ChevronDown size={16} class="cx-chevron" aria-hidden="true" />
<!-- 15-B4: with toasts routed drawer-only, a CLOSED drawer hid them
entirely — surface the same count the Toasts tab shows. -->
{#if hiddenToastCount > 0}
<span class="cx-toast-badge" class:req={$pendingApprovals.length > 0} data-testid="connect-toast-badge"
>{hiddenToastCount > 9 ? '9+' : hiddenToastCount}</span
>
{/if}
{#if srv?.didFallback}
<span class="cx-info-warn" data-testid="connect-info-warn" title="Self-hosted server unreachable — on the public cloud"></span>
{/if}
Expand Down Expand Up @@ -418,6 +431,30 @@
.cx-toggle.open :global(.cx-chevron) {
transform: rotate(180deg);
}
/* B4: live count of toasts the closed drawer is holding (mirrors the drawer's
own .cxd-tab-badge); sits opposite the amber server dot so both can show */
.cx-toast-badge {
position: absolute;
top: -5px;
left: -5px;
min-width: 15px;
height: 15px;
padding: 0 3px;
border-radius: 9999px;
background: var(--color-primary-600, #2563eb);
color: #fff;
border: 1.5px solid rgb(31 41 55);
font-size: 9px;
font-weight: 700;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
}
.cx-toast-badge.req {
background: #f59e0b;
color: #1f2937;
}
.cx-info-warn {
position: absolute;
top: -2px;
Expand Down
106 changes: 60 additions & 46 deletions src/components/menu/Inspector.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
removeObjectTexture,
setMaterialParam,
switchMaterialType,
recordMaterialChange
recordMaterialChange,
setObjectColor
} from '$lib/materialsHandler';
import { recordEntry } from '$lib/history';
import { bottomInset } from '$lib/bottomDock';
Expand Down Expand Up @@ -69,7 +70,8 @@
deleteEnvPreset,
exportEnvPreset,
importEnvPreset,
applyCustomPreset
applyCustomPreset,
editEnvSky
} from '$lib/environment';
import {
globalScene,
Expand Down Expand Up @@ -426,7 +428,10 @@
// ---- scene target (fog state is local, like the old scene panel) --------
// fogColor is INITIALIZED: svelte 5.56 hard-errors on `bind:hex={undefined}`
// when the prop has a fallback (props_invalid_value) — undefined here
// CRASHED the whole scene drawer (pre-existing on release/1.1, deps bump)
// CRASHED the whole scene drawer (pre-existing on release/1.1, deps bump).
// 15-C: the pickers pass `hex` ONE-WAY now (color-picker v4 writes its own
// snapshot back through a binding and clobbers external writes — an env
// preset or a selection change); `onInput` is the input channel.
/** @type {any} */
let fogColor = $state('#ffffff');
/** @type {any} */
Expand Down Expand Up @@ -465,15 +470,23 @@
}
event.target.value = '';
}
function sendBackgroundColor() {
$peers.send({ type: 'color', uuid: 'background', color: $backgroundColor });
}
function sendFogColor() {
$peers.send({ type: 'color', uuid: 'fog', color: fogColor, near: fogNear, far: fogFar });
// 15-C: sky edits go through the ENVIRONMENT (editEnvSky detaches a live
// custom preset). Writing scene.background / scene.fog directly was undone
// by the next applyEnvironment() — the reason "changing the background did
// nothing" survived even after the picker's dead handler was fixed. The env
// commit also persists + replicates ({type:'environment'}), so peers and a
// reload keep the color.
/** @param {string} hex */
function setBackground(hex) {
backgroundColor.set(hex);
editEnvSky({ background: hex });
}
function applyFog() {
$globalScene.fog = new THREE.Fog(fogColor ?? '#ffffff', fogNear, fogFar);
sendFogColor();
editEnvSky(
fogNear === null || fogFar === null
? { fog: null }
: { fog: { color: fogColor ?? '#ffffff', near: fogNear, far: fogFar } }
);
}
</script>

Expand Down Expand Up @@ -892,7 +905,8 @@
<Section label="Background">
<ColorPicker
isAlpha={false}
isTextInput={false}
isTextInput={true}
textInputModes={['hex', 'rgb', 'hsv']}
isDialog={false}
components={{ ...ChromeVariant, wrapper: CustomWrapper }}
isOpen={true}
Expand All @@ -903,31 +917,24 @@
--picker-height="70px"
--picker-width="50px"
--slider-width="10px"
bind:hex={$backgroundColor}
on:input={(event) => {
$backgroundColor = event.detail.hex;
$globalScene.background = new THREE.Color($backgroundColor);
sendBackgroundColor();
}}
hex={$backgroundColor}
onInput={(/** @type {any} */ c) => setBackground(c.hex)}
/>
<input
type="text"
class="ui-input w-full"
value={$backgroundColor}
onchange={(e) => {
if (hexColor.test(e.currentTarget.value)) {
$backgroundColor = e.currentTarget.value;
$globalScene.background = new THREE.Color($backgroundColor);
sendBackgroundColor();
}
if (hexColor.test(e.currentTarget.value)) setBackground(e.currentTarget.value);
}}
/>
</Section>

<Section label="Fog">
<ColorPicker
isAlpha={false}
isTextInput={false}
isTextInput={true}
textInputModes={['hex', 'rgb', 'hsv']}
isDialog={false}
components={{ ...ChromeVariant, wrapper: CustomWrapper }}
isOpen={true}
Expand All @@ -938,9 +945,9 @@
--picker-height="70px"
--picker-width="50px"
--slider-width="10px"
bind:hex={fogColor}
on:input={(event) => {
fogColor = event.detail.hex;
hex={fogColor}
onInput={(/** @type {any} */ c) => {
fogColor = c.hex;
applyFog();
}}
/>
Expand All @@ -963,10 +970,9 @@
size="xs"
color="alternative"
onclick={() => {
$globalScene.fog = null;
fogNear = null;
fogFar = null;
sendFogColor();
editEnvSky({ fog: null });
}}>Remove Fog</Button
>
</Section>
Expand Down Expand Up @@ -1169,7 +1175,8 @@
<Section label="Light">
<ColorPicker
isAlpha={false}
isTextInput={false}
isTextInput={true}
textInputModes={['hex', 'rgb', 'hsv']}
isDialog={false}
components={{ ...ChromeVariant, wrapper: CustomWrapper }}
isOpen={true}
Expand All @@ -1180,10 +1187,10 @@
--picker-height="70px"
--picker-width="50px"
--slider-width="10px"
bind:hex={color}
on:input={(event) => {
$selectedObject.color.set(event.detail.hex);
color = event.detail.hex;
hex={color}
onInput={(/** @type {any} */ c) => {
$selectedObject.color.set(c.hex);
color = c.hex;
sendLightUpdate();
}}
/>
Expand All @@ -1203,7 +1210,8 @@
<p class="ui-section-label">Ground color</p>
<ColorPicker
isAlpha={false}
isTextInput={false}
isTextInput={true}
textInputModes={['hex', 'rgb', 'hsv']}
isDialog={false}
components={{ ...ChromeVariant, wrapper: CustomWrapper }}
isOpen={true}
Expand All @@ -1214,10 +1222,10 @@
--picker-height="70px"
--picker-width="50px"
--slider-width="10px"
bind:hex={groundColor}
on:input={(event) => {
$selectedObject.groundColor.set(event.detail.hex);
groundColor = event.detail.hex;
hex={groundColor}
onInput={(/** @type {any} */ c) => {
$selectedObject.groundColor.set(c.hex);
groundColor = c.hex;
sendLightUpdate();
}}
/>
Expand Down Expand Up @@ -1333,7 +1341,8 @@
{#if material.color && material.type !== 'MeshNormalMaterial'}
<ColorPicker
isAlpha={false}
isTextInput={false}
isTextInput={true}
textInputModes={['hex', 'rgb', 'hsv']}
isDialog={false}
components={{ ...ChromeVariant, wrapper: CustomWrapper }}
isOpen={true}
Expand All @@ -1344,11 +1353,15 @@
--picker-height="70px"
--picker-width="50px"
--slider-width="10px"
bind:hex={color}
on:input={(event) => {
trackColorGesture($selectedObject.uuid, event.detail.hex);
$selectedObject.material.color.set(event.detail.hex);
$peers.send({ type: 'color', uuid: $selectedObject.uuid, color: event.detail.hex });
hex={color}
onInput={(/** @type {any} */ c) => {
// live drag: ONE debounced undo entry per gesture (setObjectColor
// would record on every frame), then apply + replicate
trackColorGesture($selectedObject.uuid, c.hex);
$selectedObject.material.color.set(c.hex);
$selectedObject.material.needsUpdate = true;
objectsGroup.update((v) => v);
$peers.send({ type: 'color', uuid: $selectedObject.uuid, color: c.hex });
}}
/>
<input
Expand All @@ -1358,8 +1371,9 @@
onchange={(e) => {
if (hexColor.test(e.currentTarget.value)) {
color = e.currentTarget.value;
$selectedObject.material.color.set(color);
$peers.send({ type: 'color', uuid: $selectedObject.uuid, color });
// a typed value is ONE discrete change — the shared write path
// applies, replicates and records a single undo entry
setObjectColor($selectedObject.uuid, color);
}
}}
/>
Expand Down
Loading