@meistrari/tela-build 1.30.4 → 1.32.0

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.
@@ -7,39 +7,54 @@ const props = defineProps<{
7
7
  to?: string
8
8
  onClick?: () => void
9
9
  isActive: boolean
10
+ disabled?: boolean
10
11
  }>()
11
12
 
12
13
  const iconName = computed(() => props.isActive ? `${props.icon}-fill` : props.icon)
14
+
15
+ const rootIs = computed(() => props.disabled || !props.to ? 'button' : NuxtLink)
16
+
17
+ const rootAttrs = computed(() => {
18
+ if (props.disabled || !props.to) {
19
+ return {
20
+ type: 'button',
21
+ disabled: props.disabled || undefined,
22
+ ...(!props.disabled && props.onClick ? { onClick: props.onClick } : {}),
23
+ }
24
+ }
25
+ return { to: props.to }
26
+ })
13
27
  </script>
14
28
 
15
29
  <template>
16
30
  <component
17
- :is="to ? NuxtLink : 'button'"
18
- :to="to"
19
- :type="!to ? 'button' : undefined"
20
- class="group"
31
+ :is="rootIs"
32
+ v-bind="rootAttrs"
33
+ class="group disabled:opacity-60 disabled:cursor-not-allowed"
21
34
  flex="~ col" items-center justify-center gap-2px outline-none
22
- :data-active="isActive"
23
- v-bind="!to && onClick ? { onClick } : {}"
35
+ :data-active="isActive && !props.disabled"
24
36
  >
25
37
  <div relative size-40px flex items-center justify-center rounded-10px>
26
38
  <TelaIcon
27
39
  :name="iconName"
28
40
  size="20px"
29
41
  relative z-1
30
- :color="isActive ? 'icon' : 'icon-tertiary duration-150 ease-out group-hover:icon group-focus-within:icon'"
42
+ color="icon-tertiary duration-150 ease-out group-[:hover:not(:disabled)]:icon group-focus-visible:icon group-data-[active=true]:icon"
31
43
  />
32
44
  <div
33
45
  :class="cn(
34
- 'absolute inset-0 size-full rounded-[14px] z-0 border-[0.5px] border-transparent',
35
- isActive ? 'bg-neutral-200 group-focus-within:border-strong' : 'bg scale-10 opacity-0 duration-150 ease-out origin-center group-hover:border-strong group-hover:scale-100 group-hover:opacity-100 group-focus-within:border-strong group-focus-within:scale-100 group-focus-within:opacity-100',
46
+ 'absolute inset-0 size-full rounded-[14px] z-0 border-[0.5px] border-transparent duration-150 ease-out origin-center group-disabled:scale-0',
47
+ 'group-data-[active=false]:bg group-data-[active=false]:scale-10 group-data-[active=false]:opacity-0',
48
+ 'group-data-[active=true]:bg-neutral-200 group-focus-visible:border-strong group-[[data-active=false]:hover]:border-strong group-hover:scale-100 group-hover:opacity-100',
49
+ 'group-focus-visible:border-strong group-focus-visible:scale-100 group-focus-visible:opacity-100',
36
50
  )"
37
51
  />
38
52
  </div>
39
53
  <p
40
54
  :class="cn(
41
- 'text-[11px] leading-[12px] -tracking-0.2px',
42
- isActive ? 'text-primary font-550' : 'font-460 text-tertiary duration-150 ease-out group-hover:text-primary group-focus-within:text-primary',
55
+ 'text-[11px] leading-[12px] -tracking-0.2px font-460 text-tertiary duration-150 ease-out',
56
+ 'group-[:hover:not(:disabled)]:text-primary group-focus-visible:text-primary',
57
+ 'group-data-[active=true]:text-primary group-data-[active=true]:font-550',
43
58
  )"
44
59
  >
45
60
  {{ label }}
@@ -57,6 +57,10 @@ A composable sidebar navigation system built from focused sub-components. Fixed
57
57
 
58
58
  <Canvas of={SidebarStories.NoActiveItem} />
59
59
 
60
+ ### Disabled Items
61
+
62
+ <Canvas of={SidebarStories.DisabledItems} />
63
+
60
64
  ### Individual Item States
61
65
 
62
66
  <Canvas of={SidebarStories.SingleItem} />
@@ -98,6 +102,7 @@ interface TelaSidebarItemProps {
98
102
  isActive: boolean // Highlights the item as the current route
99
103
  to?: string // Route path — renders as a link
100
104
  onClick?: () => void // Click handler when not using `to`
105
+ disabled?: boolean // Renders as a non-interactive button with reduced opacity
101
106
  }
102
107
  ```
103
108
 
@@ -168,6 +168,66 @@ export const ActivityActive: Story = {
168
168
  }),
169
169
  }
170
170
 
171
+ export const DisabledItems: Story = {
172
+ parameters: {
173
+ docs: {
174
+ description: {
175
+ story: 'Disabled items render as non-interactive buttons with reduced opacity. The `to` prop is ignored when `disabled` is `true`.',
176
+ },
177
+ },
178
+ },
179
+ render: () => ({
180
+ components,
181
+ setup() {
182
+ return { userActions }
183
+ },
184
+ template: `
185
+ <TelaSidebar>
186
+ <TelaSidebarHeader>
187
+ <TelaSidebarLogo src="/tela-logo-black.svg" alt="Tela Logo" />
188
+ </TelaSidebarHeader>
189
+
190
+ <TelaSidebarContent>
191
+ <TelaSidebarItem
192
+ icon="i-ph-house"
193
+ label="Home"
194
+ to="/"
195
+ :is-active="true"
196
+ />
197
+ <TelaSidebarItem
198
+ icon="i-ph-graph"
199
+ label="Workflows"
200
+ to="/workflows"
201
+ :is-active="false"
202
+ disabled
203
+ />
204
+ <TelaSidebarItem
205
+ icon="i-ph-database"
206
+ label="Data"
207
+ to="/data"
208
+ :is-active="false"
209
+ disabled
210
+ />
211
+ <TelaSidebarItem
212
+ icon="i-ph-gear"
213
+ label="Settings"
214
+ to="/settings"
215
+ :is-active="false"
216
+ />
217
+ </TelaSidebarContent>
218
+
219
+ <TelaSidebarFooter>
220
+ <TelaSidebarUser
221
+ name="Username"
222
+ email="user@example.com"
223
+ :actions="userActions"
224
+ />
225
+ </TelaSidebarFooter>
226
+ </TelaSidebar>
227
+ `,
228
+ }),
229
+ }
230
+
171
231
  export const SingleItem: Story = {
172
232
  parameters: {
173
233
  layout: 'centered',
@@ -97,7 +97,9 @@ function clearAll() {
97
97
  </template>
98
98
  ```
99
99
 
100
- ### Read-Only Display
100
+ ### Non-Editable (selection only)
101
+
102
+ Pass `:is-editable="false"` to hide the create button (`+`) and the per-tag edit pencil. Users can still select/deselect existing tags, but cannot create new ones or modify existing ones.
101
103
 
102
104
  ```vue
103
105
  <script setup>
@@ -112,6 +114,7 @@ const tags = ref([
112
114
  <TelaTagsSelect
113
115
  :model-value="tags"
114
116
  :options="tags"
117
+ :is-editable="false"
115
118
  />
116
119
  </template>
117
120
  ```
@@ -143,6 +146,7 @@ type TagsSelectProps = {
143
146
  addNewNamePlaceholder?: string
144
147
  createButtonLabel?: string
145
148
  saveChangesButtonLabel?: string
149
+ isEditable?: boolean // Default: true. When false, hides the create (+) button and the per-tag edit pencil.
146
150
  }
147
151
  ```
148
152
 
@@ -24,6 +24,7 @@ const props = withDefaults(defineProps<{
24
24
  assignLabel?: string
25
25
  showLabel?: boolean
26
26
  alwaysShowEmpty?: boolean
27
+ isEditable?: boolean
27
28
  }>(), {
28
29
  modelValue: () => [],
29
30
  disabled: false,
@@ -41,6 +42,7 @@ const props = withDefaults(defineProps<{
41
42
  tagPlural: 'tags',
42
43
  showLabel: true,
43
44
  alwaysShowEmpty: false,
45
+ isEditable: true,
44
46
  })
45
47
 
46
48
  const emit = defineEmits<{
@@ -141,7 +143,7 @@ function resolveColor(color: string, lightColor = false): string {
141
143
  const allCreatedTags = ref<Tag[]>([])
142
144
  const localSelectedTags = ref<Tag[]>(props.modelValue || [])
143
145
  const popoverOpen = ref(false)
144
- const selectedContent = ref<ContentType>(allCreatedTags.value.length > 0 ? 'existing-tag' : 'new-tag')
146
+ const selectedContent = ref<ContentType>(allCreatedTags.value.length > 0 || !props.isEditable ? 'existing-tag' : 'new-tag')
145
147
  const newTagName = ref<string>('')
146
148
  const selectedColorsForNewTag = ref<string | null>(ALL_COLORS[0])
147
149
  const selectedTagsForApply = ref<Set<string>>(new Set())
@@ -247,7 +249,7 @@ watch(() => props.modelValue, (newTags) => {
247
249
 
248
250
  watch(popoverOpen, (isOpen) => {
249
251
  if (isOpen) {
250
- selectedContent.value = hasTags.value ? 'existing-tag' : 'new-tag'
252
+ selectedContent.value = hasTags.value || !props.isEditable ? 'existing-tag' : 'new-tag'
251
253
  selectedTagsForApply.value = getTagKeys(localSelectedTags.value)
252
254
  }
253
255
  else {
@@ -574,7 +576,7 @@ updateAllCreatedTags()
574
576
  <h5 heading-h5-semibold>
575
577
  {{ allCreatedTags.length }} {{ allCreatedTags.length > 1 ? props.tagPlural : props.tagSingular }}
576
578
  </h5>
577
- <button flex items-center justify-center w-24px h-24px rounded-6px hover:bg-background-muted mr--4px @click="handleCreateMore">
579
+ <button v-if="props.isEditable" flex items-center justify-center w-24px h-24px rounded-6px hover:bg-background-muted mr--4px @click="handleCreateMore">
578
580
  <TelaIcon name="i-ph-plus" size="16px" color="icon" />
579
581
  </button>
580
582
  </div>
@@ -588,6 +590,7 @@ updateAllCreatedTags()
588
590
  </div>
589
591
  <div flex items-center gap-4px>
590
592
  <button
593
+ v-if="props.isEditable"
591
594
  class="group opacity-0 group-hover/tag:opacity-100" flex items-center justify-center w-17px h-17px rounded-5px hover:bg-background-lowered
592
595
  @click.stop="handleEditTag(tag)"
593
596
  >
@@ -0,0 +1,71 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'pathe'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+
6
+ import { resolveRepositoryRoot } from '../index'
7
+
8
+ const tempDirs: string[] = []
9
+
10
+ function makeTempRoot() {
11
+ const dir = mkdtempSync(join(tmpdir(), 'resolve-repository-root-'))
12
+ tempDirs.push(dir)
13
+
14
+ return dir
15
+ }
16
+
17
+ afterEach(() => {
18
+ for (const dir of tempDirs.splice(0)) {
19
+ rmSync(dir, { recursive: true, force: true })
20
+ }
21
+ })
22
+
23
+ describe('resolveRepositoryRoot', () => {
24
+ it('prefers Nuxt workspaceDir for monorepos running without .git in Docker', () => {
25
+ const workspaceRoot = makeTempRoot()
26
+ const appDir = join(workspaceRoot, 'apps/web')
27
+
28
+ mkdirSync(appDir, { recursive: true })
29
+
30
+ expect(resolveRepositoryRoot(appDir, workspaceRoot)).toBe(workspaceRoot)
31
+ })
32
+
33
+ it('detects workspace roots from .git markers', () => {
34
+ const workspaceRoot = makeTempRoot()
35
+ const appDir = join(workspaceRoot, 'apps/web')
36
+
37
+ mkdirSync(appDir, { recursive: true })
38
+ writeFileSync(join(workspaceRoot, '.git'), 'gitdir: ../.git/worktrees/test\n')
39
+
40
+ expect(resolveRepositoryRoot(appDir)).toBe(workspaceRoot)
41
+ })
42
+
43
+ it('detects workspace roots from pnpm-workspace.yaml', () => {
44
+ const workspaceRoot = makeTempRoot()
45
+ const appDir = join(workspaceRoot, 'apps/web')
46
+
47
+ mkdirSync(appDir, { recursive: true })
48
+ writeFileSync(join(workspaceRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n')
49
+
50
+ expect(resolveRepositoryRoot(appDir)).toBe(workspaceRoot)
51
+ })
52
+
53
+ it('detects workspace roots from package.json workspaces when workspaceDir is unavailable', () => {
54
+ const workspaceRoot = makeTempRoot()
55
+ const appDir = join(workspaceRoot, 'apps/web')
56
+
57
+ mkdirSync(appDir, { recursive: true })
58
+ writeFileSync(join(workspaceRoot, 'package.json'), JSON.stringify({
59
+ private: true,
60
+ workspaces: ['apps/*', 'packages/*'],
61
+ }))
62
+
63
+ expect(resolveRepositoryRoot(appDir)).toBe(workspaceRoot)
64
+ })
65
+
66
+ it('falls back to the app root when no repository markers exist', () => {
67
+ const appDir = makeTempRoot()
68
+
69
+ expect(resolveRepositoryRoot(appDir)).toBe(appDir)
70
+ })
71
+ })
@@ -72,7 +72,7 @@ export default defineNuxtModule<TelaBuildDocsOptions>({
72
72
  // Run in background without blocking
73
73
  setImmediate(async () => {
74
74
  try {
75
- const repositoryRoot = resolveRepositoryRoot(nuxt.options.rootDir)
75
+ const repositoryRoot = resolveRepositoryRoot(nuxt.options.rootDir, nuxt.options.workspaceDir)
76
76
  try {
77
77
  if (!options.outDir) {
78
78
  ensureClaudeSymlink(repositoryRoot, logger, colors)
@@ -175,11 +175,15 @@ function resolveOutputDirectories(repositoryRoot: string, outDir?: string): stri
175
175
  return [resolve(repositoryRoot, '.agents/skills')]
176
176
  }
177
177
 
178
- function resolveRepositoryRoot(startDir: string): string {
178
+ export function resolveRepositoryRoot(startDir: string, workspaceDir?: string): string {
179
+ if (workspaceDir && existsSync(resolve(workspaceDir))) {
180
+ return resolve(workspaceDir)
181
+ }
182
+
179
183
  let currentDir = resolve(startDir)
180
184
 
181
185
  while (true) {
182
- if (existsSync(resolve(currentDir, '.git')) || existsSync(resolve(currentDir, 'pnpm-workspace.yaml'))) {
186
+ if (isRepositoryRoot(currentDir)) {
183
187
  return currentDir
184
188
  }
185
189
 
@@ -192,6 +196,25 @@ function resolveRepositoryRoot(startDir: string): string {
192
196
  }
193
197
  }
194
198
 
199
+ function isRepositoryRoot(directory: string): boolean {
200
+ if (existsSync(resolve(directory, '.git')) || existsSync(resolve(directory, 'pnpm-workspace.yaml'))) {
201
+ return true
202
+ }
203
+
204
+ const packageJsonPath = resolve(directory, 'package.json')
205
+ if (!existsSync(packageJsonPath)) {
206
+ return false
207
+ }
208
+
209
+ try {
210
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { workspaces?: unknown }
211
+ return packageJson.workspaces !== undefined
212
+ }
213
+ catch {
214
+ return false
215
+ }
216
+ }
217
+
195
218
  function ensurePathsIgnored(repositoryRoot: string, targets: IgnoreTarget[]): void {
196
219
  const gitignorePath = resolve(repositoryRoot, '.gitignore')
197
220
  const existingLines = existsSync(gitignorePath)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.30.4",
3
+ "version": "1.32.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",