{
+ it('anchors a window to the platform top-right corner', () => {
+ expect(createContainerAnchorStyle('top-right')).toEqual({
+ right: '0px',
+ top: '0px',
+ })
+ })
+
+ it('centers a window in the platform', () => {
+ expect(createContainerAnchorStyle('center')).toEqual({
+ left: '50%',
+ top: '50%',
+ transform: 'translate(-50%, -50%)',
+ })
+ })
+
+ it('anchors a window to the usable area left of a vertical dock', () => {
+ expect(createContainerAnchorStyle('bottom-right', {
+ left: 0,
+ top: 0,
+ width: 1720,
+ height: 1080,
+ }, {
+ width: 1920,
+ height: 1080,
+ })).toEqual({
+ bottom: '0px',
+ right: '200px',
+ })
+ })
+})
+
+describe('createWorkAreaRect', () => {
+ it('shrinks the work area from the right edge for a vertical dock', () => {
+ expect(createWorkAreaRect({
+ platformRect: {
+ left: 0,
+ top: 0,
+ width: 1920,
+ height: 1080,
+ },
+ dockRect: {
+ left: 1720,
+ top: 240,
+ width: 160,
+ height: 600,
+ },
+ })).toEqual({
+ left: 0,
+ top: 0,
+ width: 1720,
+ height: 1080,
+ })
+ })
+})
+
+describe('computeElementAnchorStyle', () => {
+ it('anchors the same window corner to an element corner inside the platform', () => {
+ expect(computeElementAnchorStyle({
+ anchor: 'bottom-right',
+ anchorRect: {
+ left: 210,
+ top: 140,
+ width: 120,
+ height: 40,
+ },
+ platformRect: {
+ left: 100,
+ top: 50,
+ },
+ windowRect: {
+ width: 80,
+ height: 30,
+ },
+ })).toEqual({
+ left: '150px',
+ top: '100px',
+ })
+ })
+})
diff --git a/packages/scenarios-stage-tamagotchi-browser/src/components/platforms/macos-26/containers/window/window-anchor.ts b/packages/scenarios-stage-tamagotchi-browser/src/components/platforms/macos-26/containers/window/window-anchor.ts
new file mode 100644
index 000000000..834a22351
--- /dev/null
+++ b/packages/scenarios-stage-tamagotchi-browser/src/components/platforms/macos-26/containers/window/window-anchor.ts
@@ -0,0 +1,159 @@
+import type { CSSProperties } from 'vue'
+
+export type WindowAnchor = 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' | 'center'
+
+type RectPosition = Pick
+type RectSize = Pick
+type RectLike = RectPosition & RectSize
+type RelativeRect = RectLike
+
+function toRelativeRect(rect: RectLike, platformRect: RectPosition): RelativeRect {
+ // All anchor math is done in the platform's own coordinate system so callers
+ // can mix measured DOM rects with the logical scene layout consistently.
+ return {
+ left: rect.left - platformRect.left,
+ top: rect.top - platformRect.top,
+ width: rect.width,
+ height: rect.height,
+ }
+}
+
+function resolveAnchorPoint(rect: RectLike, anchor: WindowAnchor) {
+ switch (anchor) {
+ case 'top-left':
+ return { x: rect.left, y: rect.top }
+ case 'top-right':
+ return { x: rect.left + rect.width, y: rect.top }
+ case 'bottom-left':
+ return { x: rect.left, y: rect.top + rect.height }
+ case 'bottom-right':
+ return { x: rect.left + rect.width, y: rect.top + rect.height }
+ case 'center':
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
+ }
+}
+
+export function createContainerAnchorStyle(
+ anchor: WindowAnchor,
+ boundsRect?: RelativeRect,
+ platformSize?: RectSize,
+): CSSProperties {
+ // `boundsRect` describes the usable anchoring area inside the platform. When
+ // it is omitted we anchor against the full platform origin, matching the
+ // original behavior before dock-aware work areas existed.
+ const left = boundsRect?.left ?? 0
+ const top = boundsRect?.top ?? 0
+ const right = boundsRect && platformSize
+ ? platformSize.width - (boundsRect.left + boundsRect.width)
+ : 0
+ const bottom = boundsRect && platformSize
+ ? platformSize.height - (boundsRect.top + boundsRect.height)
+ : 0
+
+ switch (anchor) {
+ case 'top-left':
+ return {
+ left: `${left}px`,
+ top: `${top}px`,
+ }
+ case 'top-right':
+ return {
+ right: `${right}px`,
+ top: `${top}px`,
+ }
+ case 'bottom-left':
+ return {
+ bottom: `${bottom}px`,
+ left: `${left}px`,
+ }
+ case 'bottom-right':
+ return {
+ bottom: `${bottom}px`,
+ right: `${right}px`,
+ }
+ case 'center':
+ return {
+ left: boundsRect && platformSize ? `${left + boundsRect.width / 2}px` : '50%',
+ top: boundsRect && platformSize ? `${top + boundsRect.height / 2}px` : '50%',
+ transform: 'translate(-50%, -50%)',
+ }
+ }
+}
+
+export function createWorkAreaRect(options: {
+ dockRect?: RectLike | null
+ platformRect: RectLike
+}): RelativeRect {
+ if (!options.dockRect) {
+ return {
+ left: 0,
+ top: 0,
+ width: options.platformRect.width,
+ height: options.platformRect.height,
+ }
+ }
+
+ const dockRect = toRelativeRect(options.dockRect, options.platformRect)
+ // A tall dock behaves like a left/right sidebar; a wide dock behaves like a
+ // top/bottom shelf. We trim the usable work area from the nearest edge so
+ // bottom/right anchors can stay clear of the dock without hard-coded offsets.
+ const isVerticalDock = dockRect.height >= dockRect.width
+ const distances = {
+ bottom: options.platformRect.height - (dockRect.top + dockRect.height),
+ left: dockRect.left,
+ right: options.platformRect.width - (dockRect.left + dockRect.width),
+ top: dockRect.top,
+ }
+
+ const workArea = {
+ left: 0,
+ top: 0,
+ width: options.platformRect.width,
+ height: options.platformRect.height,
+ }
+
+ if (isVerticalDock) {
+ if (distances.left <= distances.right) {
+ const inset = dockRect.left + dockRect.width
+ workArea.left = inset
+ workArea.width = options.platformRect.width - inset
+ }
+ else {
+ workArea.width = dockRect.left
+ }
+ }
+ else if (distances.top <= distances.bottom) {
+ const inset = dockRect.top + dockRect.height
+ workArea.top = inset
+ workArea.height = options.platformRect.height - inset
+ }
+ else {
+ workArea.height = dockRect.top
+ }
+
+ return workArea
+}
+
+export function computeElementAnchorStyle(options: {
+ anchor: WindowAnchor
+ anchorRect: RectLike
+ platformRect: RectPosition
+ windowRect: RectSize
+}): CSSProperties {
+ const relativeAnchorRect = toRelativeRect(options.anchorRect, options.platformRect)
+ const anchorPoint = resolveAnchorPoint(relativeAnchorRect, options.anchor)
+ // We place the window by subtracting its own anchor point from the target's
+ // anchor point. That keeps "bottom-right to bottom-right" and similar cases
+ // aligned without requiring separate formulas per anchor variant.
+ const windowPoint = resolveAnchorPoint({
+ left: 0,
+ top: 0,
+ width: options.windowRect.width,
+ height: options.windowRect.height,
+ }, options.anchor)
+
+ return {
+ left: `${anchorPoint.x - windowPoint.x}px`,
+ top: `${anchorPoint.y - windowPoint.y}px`,
+ }
+}
diff --git a/packages/scenarios-stage-tamagotchi-browser/src/components/platforms/macos-26/ui/dock/dock.vue b/packages/scenarios-stage-tamagotchi-browser/src/components/platforms/macos-26/ui/dock/dock.vue
index 67f2b2321..e8fb06b9b 100644
--- a/packages/scenarios-stage-tamagotchi-browser/src/components/platforms/macos-26/ui/dock/dock.vue
+++ b/packages/scenarios-stage-tamagotchi-browser/src/components/platforms/macos-26/ui/dock/dock.vue
@@ -1,4 +1,7 @@
-
+
-
+
diff --git a/packages/scenarios-stage-tamagotchi-browser/vitest.config.ts b/packages/scenarios-stage-tamagotchi-browser/vitest.config.ts
new file mode 100644
index 000000000..c3292e3d1
--- /dev/null
+++ b/packages/scenarios-stage-tamagotchi-browser/vitest.config.ts
@@ -0,0 +1,15 @@
+import Vue from '@vitejs/plugin-vue'
+import Unocss from 'unocss/vite'
+
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ root: import.meta.dirname,
+ plugins: [
+ Vue(),
+ Unocss(),
+ ],
+ test: {
+ include: ['src/**/*.test.ts'],
+ },
+})