Skip to content
Open
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
11 changes: 11 additions & 0 deletions ui/prototype/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/prototype/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@tailwindcss/vite": "4.2.2",
"@vitejs/plugin-vue": "^6.0.5",
"tailwind-merge": "^3.5.0",
"tailwindcss": "4.2.2",
"typescript": "^5.9.3",
"vite": "^8.0.0",
Expand Down
38 changes: 0 additions & 38 deletions ui/prototype/scripts/check-prototype.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import { join, relative } from 'node:path'
const root = new URL('..', import.meta.url).pathname
const srcRoot = join(root, 'src')

function read(path) {
return readFileSync(join(root, path), 'utf8')
}

function walk(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name)
Expand All @@ -19,40 +15,6 @@ function walk(dir) {

const failures = []
const sourceFiles = walk(srcRoot).filter((path) => /\.(ts|vue|css)$/.test(path))
const prototypeBlockItem = read('src/components/editor/UIBlockItem.vue')
const prototypeBlockItemTitle = read('src/components/editor/UIBlockItemTitle.vue')
const prototypeEditorSpriteItem = read('src/components/editor/UIEditorSpriteItem.vue')
const prototypeSpriteItem = read('src/components/editor/SpriteItem.vue')

if (
prototypeBlockItem.includes('<button') ||
!prototypeBlockItem.includes('<div') ||
!prototypeBlockItem.includes('.ui-block-item-active::before') ||
!prototypeBlockItem.includes('border-width: 2px;')
) {
failures.push('prototype UIBlockItem must mirror the real block item root and keep a 2px active pseudo-border')
}

if (
!prototypeSpriteItem.includes("import UIEditorSpriteItem from '@/components/editor/UIEditorSpriteItem.vue'") ||
!prototypeSpriteItem.includes('<UIEditorSpriteItem') ||
prototypeSpriteItem.includes("import eyeOffIcon from '@/assets/editor/ui-icons/eye-off.svg?raw'")
) {
failures.push('prototype SpriteItem must reuse UIEditorSpriteItem instead of duplicating title and hidden icon layout')
}

if (
!prototypeBlockItemTitle.includes('w-full') ||
!prototypeBlockItemTitle.includes('px-1.5') ||
!prototypeEditorSpriteItem.includes('<UIBlockItemTitle class="gap-0.5 px-1"') ||
prototypeEditorSpriteItem.includes('w-[76px]') ||
prototypeEditorSpriteItem.includes('width: 76px') ||
prototypeEditorSpriteItem.includes('width: calc(100% - 8px)') ||
prototypeEditorSpriteItem.includes('px-0') ||
prototypeEditorSpriteItem.includes('title="Invisible"')
) {
failures.push('prototype UIEditorSpriteItem title row must use width 100% with 4px padding and no hidden-icon tooltip override')
}

for (const file of sourceFiles) {
const text = readFileSync(file, 'utf8')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structural check that SpriteItem.vue imports and uses UIEditorSpriteItem (rather than duplicating its own hidden-icon layout) was a valid regression guard — the regression it prevented was precisely the one that necessitated PR #3198. It was removed alongside the bad CSS checks, but the two concerns are independent. Worth restoring just the structural checks here:

const prototypeSpriteItem = readFileSync(join(root, 'src/components/editor/SpriteItem.vue'), 'utf8')
if (
  !prototypeSpriteItem.includes("import UIEditorSpriteItem from '@/components/editor/UIEditorSpriteItem.vue'") ||
  !prototypeSpriteItem.includes('<UIEditorSpriteItem')
) {
  failures.push('prototype SpriteItem must delegate to UIEditorSpriteItem instead of duplicating its layout')
}

Expand Down
2 changes: 1 addition & 1 deletion ui/prototype/src/components/editor/AnimationItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import UIEditorSpriteItem from '@/components/editor/UIEditorSpriteItem.vue'
import UIEditorSpriteItem from '@/components/ui/block-items/UIEditorSpriteItem.vue'

defineProps<{
animation: {
Expand Down
2 changes: 1 addition & 1 deletion ui/prototype/src/components/editor/CostumeItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import UIEditorSpriteItem from '@/components/editor/UIEditorSpriteItem.vue'
import UIEditorSpriteItem from '@/components/ui/block-items/UIEditorSpriteItem.vue'

defineProps<{
costume: {
Expand Down
5 changes: 2 additions & 3 deletions ui/prototype/src/components/editor/SpriteItem.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script setup lang="ts">
import UIEditorSpriteItem from '@/components/editor/UIEditorSpriteItem.vue'
import UIEditorSpriteItem from '@/components/ui/block-items/UIEditorSpriteItem.vue'

defineProps<{
sprite: {
name: string
shortName: string
image: string
hidden: boolean
}
Expand All @@ -17,7 +16,7 @@ const emit = defineEmits<{
</script>

<template>
<UIEditorSpriteItem :name="sprite.shortName" :title="sprite.name" :selected="active" :visible="!sprite.hidden" @click="emit('select')">
<UIEditorSpriteItem :name="sprite.name" :selected="active" :visible="!sprite.hidden" @click="emit('select')">
<template #img="{ style }">
<img :src="sprite.image" :alt="sprite.name" :style="style" />
</template>
Expand Down
16 changes: 10 additions & 6 deletions ui/prototype/src/components/ui/UITab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import { computed } from 'vue'

import { useUITabsCtx } from './UITabs.vue'
import { cn, type ClassValue } from './utils'

const props = withDefaults(
defineProps<{
value: string
class?: string
class?: ClassValue
}>(),
{
class: ''
Expand All @@ -15,15 +16,18 @@ const props = withDefaults(

const tabsCtx = useUITabsCtx()
const active = computed(() => tabsCtx.value.value === props.value)
const rootClass = computed(() =>
cn(
'tab flex h-full min-w-0 cursor-pointer items-center overflow-hidden border-b-2 px-md text-xl/8 whitespace-nowrap transition-[color,border-color] duration-200',
active.value ? 'border-grey-1000 text-grey-1000' : 'border-transparent text-grey-800 hover:text-grey-1000',
props.class
)
)
</script>

<template>
<li
class="tab flex h-full min-w-0 cursor-pointer items-center overflow-hidden border-b-2 px-md text-xl/8 whitespace-nowrap transition-[color,border-color] duration-200"
:class="[
active ? 'border-grey-1000 text-grey-1000' : 'border-transparent text-grey-800 hover:text-grey-1000',
props.class
]"
:class="rootClass"
@click="tabsCtx.setValue(props.value)"
>
<slot />
Expand Down
8 changes: 6 additions & 2 deletions ui/prototype/src/components/ui/UITabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ export function useUITabsCtx() {
<script setup lang="ts">
import { computed, provide } from 'vue'

import { cn, type ClassValue } from './utils'

const props = withDefaults(
defineProps<{
value: string
class?: string
class?: ClassValue
}>(),
{
class: ''
Expand All @@ -38,10 +40,12 @@ provide(prototypeTabsCtxKey, {
emit('update:value', value)
}
})

const rootClass = computed(() => cn('m-0 flex min-w-0 list-none overflow-hidden px-2', props.class))
</script>

<template>
<ul class="m-0 flex min-w-0 list-none overflow-hidden px-2" :class="props.class">
<ul :class="rootClass">
<slot />
</ul>
</template>
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
<template>
<div :class="rootClass">
<span class="min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap" :title="title">
<slot></slot>
</span>
<slot v-if="slots.suffix != null" name="suffix"></slot>
</div>
</template>

<script setup lang="ts">
import { computed, useSlots } from 'vue'

import { cn, type ClassValue } from '../utils'

const props = defineProps<{
size: 'medium' | 'large'
title: string
class?: string
class?: ClassValue
}>()

const slots = useSlots()
const rootClass = computed(() => [
'flex w-full items-center gap-2 px-1.5 text-center text-title',
props.size === 'large' ? 'h-5 text-sm' : 'h-5.5 text-2xs',
props.class
])
const rootClass = computed(() =>
cn(
'flex items-center text-title',
props.size === 'large'
? 'h-5 w-full gap-2 px-1.5 text-center text-sm'
: 'h-5.5 w-full gap-0.5 px-1 text-center text-2xs',
props.class
)
)
</script>

<template>
<div :class="rootClass">
<span class="min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap" :title="title">
<slot></slot>
</span>
<slot v-if="slots.suffix != null" name="suffix"></slot>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<UIBlockItem :active="selected">
<div class="mt-0.5 flex min-h-0 w-full flex-col items-center">
<img class="h-15 w-20 rounded-[4px] object-cover" :src="imgSrc" :alt="name" />
</div>
<UIBlockItemTitle size="medium" :title="name">
{{ name }}
</UIBlockItemTitle>
<slot></slot>
</UIBlockItem>
</template>

<script setup lang="ts">
import UIBlockItem from '@/components/ui/block-items/UIBlockItem.vue'
import UIBlockItemTitle from '@/components/ui/block-items/UIBlockItemTitle.vue'

defineProps<{
imgSrc: string
name: string
selected?: boolean
}>()
</script>
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import eyeOffIcon from '@/assets/editor/ui-icons/eye-off.svg?raw'
import UIBlockItem from '@/components/editor/UIBlockItem.vue'
import UIBlockItemTitle from '@/components/editor/UIBlockItemTitle.vue'
import UIBlockItem from '@/components/ui/block-items/UIBlockItem.vue'
import UIBlockItemTitle from '@/components/ui/block-items/UIBlockItemTitle.vue'
import UIIcon from '@/components/ui/icons/UIIcon.vue'

defineProps<{
name: string
title?: string
selected?: boolean
visible?: boolean
}>()
withDefaults(
defineProps<{
name: string
title?: string
selected?: boolean
visible?: boolean | null
}>(),
{
visible: null
}
)

const imgStyle: CSSProperties = {
width: '60px',
Expand All @@ -23,15 +28,16 @@ const imgStyle: CSSProperties = {
<UIBlockItem :active="selected">
<slot name="img" :style="imgStyle"></slot>

<UIBlockItemTitle class="gap-0.5 px-1" size="medium" :title="title ?? name">
<UIBlockItemTitle size="medium" :title="title ?? name">
{{ name }}
<template v-if="visible === false" #suffix>
<span
<UIIcon
class="size-3.5 flex-none cursor-auto text-grey-700"
type="eyeOff"
role="img"
aria-label="Invisible"
v-html="eyeOffIcon"
></span>
title="Invisible"
/>
</template>
</UIBlockItemTitle>
<slot></slot>
Expand Down
31 changes: 31 additions & 0 deletions ui/prototype/src/components/ui/icons/UIIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div :class="rootClass" :data-icon-type="type" v-html="typeIconMap[type]"></div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

import { cn, type ClassValue } from '../utils'
import eyeOff from './eye-off.svg?raw'

const typeIconMap = {
eyeOff
}

export type Type = keyof typeof typeIconMap

const props = defineProps<{
type: Type
class?: ClassValue
}>()

const rootClass = computed(() => cn('ui-icon flex h-4 w-4', props.class))
</script>

<style scoped>
.ui-icon :deep(svg) {
width: 100%;
height: 100%;
}
</style>
3 changes: 3 additions & 0 deletions ui/prototype/src/components/ui/icons/eye-off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions ui/prototype/src/components/ui/utils/cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { twMerge } from 'tailwind-merge'

export type ClassDictionary = Record<string, boolean | null>
export type ClassValue = string | ClassDictionary | null | undefined | false | ClassValue[]

function flattenClassValue(value: ClassValue, tokens: string[]) {
if (value == null || value === false) return

if (typeof value === 'string') {
tokens.push(value)
return
}

if (Array.isArray(value)) {
for (const item of value) flattenClassValue(item, tokens)
return
}

for (const [className, enabled] of Object.entries(value)) {
if (enabled === true) tokens.push(className)
}
}

export function cn(...values: ClassValue[]) {
const tokens: string[] = []
for (const value of values) flattenClassValue(value, tokens)
return twMerge(tokens.join(' '))
}
1 change: 1 addition & 0 deletions ui/prototype/src/components/ui/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cn'
Loading