@meistrari/tela-build 1.62.1 → 1.64.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.
@@ -9,6 +9,7 @@ const props = defineProps<{
9
9
  projectLabel: string
10
10
  projectsLabel: string
11
11
  selectedLabel: string
12
+ loading?: boolean
12
13
  }>()
13
14
 
14
15
  const emit = defineEmits<{
@@ -24,39 +25,58 @@ function formatStat(count: number | undefined, singular: string, plural: string)
24
25
  </script>
25
26
 
26
27
  <template>
27
- <button
28
- v-for="workspace of workspaces"
29
- :key="workspace.id"
30
- h-64px
31
- flex w-full items-center px-16px
32
- hover:bg-muted
33
- first:rounded-t-12px
34
- last:rounded-b-12px
35
- not-last:border-b="0.5px border"
36
- data-testid="tela.switchWorkspace.workspaceButton"
37
- @click="emit('select', workspace)"
38
- >
39
- <div mr-12px :class="activeWorkspaceId === workspace.id && 'ring-1.5px ring-offset-2px ring-gray-400 rounded-10px'">
40
- <TelaAvatar size="md" :image="workspace.logo ?? undefined" :alt="workspace.name" class="!rounded-[10px]" />
41
- </div>
28
+ <template v-if="loading">
29
+ <div
30
+ v-for="index in 3"
31
+ :key="index"
32
+ h-64px
33
+ flex w-full items-center px-16px
34
+ not-last:border-b="0.5px border"
35
+ >
36
+ <TelaSkeleton mr-12px h-40px w-40px rounded-10px />
42
37
 
43
- <div flex="~ col" gap-2px>
44
- <h5 heading-h5-semibold text-primary text-start>
45
- {{ workspace.name }}
46
- </h5>
47
- <div flex items-center gap-4px>
48
- <p body-12-regular text-tertiary>
49
- {{ formatStat(workspace.stats?.memberCount, props.memberLabel, props.membersLabel) }}
50
- </p>
51
- <div aria-hidden="true" w-3px h-3px rounded-full bg-gray-200 />
52
- <p body-12-regular text-tertiary>
53
- {{ formatStat(workspace.stats?.projectCount, props.projectLabel, props.projectsLabel) }}
54
- </p>
38
+ <div flex="~ col" gap-8px>
39
+ <TelaSkeleton h-14px w-150px rounded-4px />
40
+ <TelaSkeleton h-12px w-96px rounded-4px />
55
41
  </div>
56
42
  </div>
43
+ </template>
44
+
45
+ <template v-else>
46
+ <button
47
+ v-for="workspace of workspaces"
48
+ :key="workspace.id"
49
+ h-64px
50
+ flex w-full items-center px-16px
51
+ hover:bg-muted
52
+ first:rounded-t-12px
53
+ last:rounded-b-12px
54
+ not-last:border-b="0.5px border"
55
+ data-testid="tela.switchWorkspace.workspaceButton"
56
+ @click="emit('select', workspace)"
57
+ >
58
+ <div mr-12px :class="activeWorkspaceId === workspace.id && 'ring-1.5px ring-offset-2px ring-gray-400 rounded-10px'">
59
+ <TelaAvatar size="md" :image="workspace.logo ?? undefined" :alt="workspace.name" class="!rounded-[10px]" />
60
+ </div>
61
+
62
+ <div flex="~ col" gap-2px>
63
+ <h5 heading-h5-semibold text-primary text-start>
64
+ {{ workspace.name }}
65
+ </h5>
66
+ <div flex items-center gap-4px>
67
+ <p body-12-regular text-tertiary>
68
+ {{ formatStat(workspace.stats?.memberCount, props.memberLabel, props.membersLabel) }}
69
+ </p>
70
+ <div aria-hidden="true" w-3px h-3px rounded-full bg-gray-200 />
71
+ <p body-12-regular text-tertiary>
72
+ {{ formatStat(workspace.stats?.projectCount, props.projectLabel, props.projectsLabel) }}
73
+ </p>
74
+ </div>
75
+ </div>
57
76
 
58
- <TelaBadge v-if="activeWorkspaceId === workspace.id" ml-auto>
59
- {{ props.selectedLabel }}
60
- </TelaBadge>
61
- </button>
77
+ <TelaBadge v-if="activeWorkspaceId === workspace.id" ml-auto>
78
+ {{ props.selectedLabel }}
79
+ </TelaBadge>
80
+ </button>
81
+ </template>
62
82
  </template>
@@ -11,6 +11,7 @@ defineProps<{
11
11
  projectLabel: string
12
12
  projectsLabel: string
13
13
  selectedLabel: string
14
+ loading?: boolean
14
15
  }>()
15
16
 
16
17
  const emit = defineEmits<{
@@ -60,6 +61,7 @@ function onClose() {
60
61
  <div overflow-y-auto no-scrollbar>
61
62
  <TelaModalSwitchWorkspaceContent
62
63
  :workspaces="workspaces"
64
+ :loading="loading"
63
65
  :active-workspace-id="activeWorkspaceId"
64
66
  :member-label="memberLabel"
65
67
  :members-label="membersLabel"
@@ -34,6 +34,33 @@ describe('generateDocsToDirectory', () => {
34
34
  expect(skillMd).toContain('description: "')
35
35
  })
36
36
 
37
+ // The pantry block was originally hand-written into the generated SKILL.md and
38
+ // would have been deleted by the next generation run. It has to come from the
39
+ // generator, and the values have to survive: Pantry derives shelf slugs from
40
+ // these strings, so lowercasing one renames a shelf shared across the catalog.
41
+ it('emits the pantry block for the tela-build skill', () => {
42
+ const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
43
+ tempDirs.push(outDir)
44
+
45
+ generateDocsToDirectory([], new TypeResolver(outDir), outDir)
46
+
47
+ const parsed = matter(readFileSync(join(outDir, 'tela-build', 'SKILL.md'), 'utf-8'))
48
+
49
+ expect(parsed.data.pantry).toEqual({
50
+ 'owner': ['guhrodrrigues'],
51
+ 'status': 'stable',
52
+ 'shelves': ['Frontend', 'Design Engineering', 'Developer Tools'],
53
+ 'use-cases': [
54
+ 'Look up a Tela Build component\'s props, slots, variants, and usage examples before writing UI code',
55
+ 'Pick the correct design token for a color, text style, border, shadow, or spacing decision',
56
+ 'Polish an existing interface\'s radius, elevation, optical alignment, typography, and animations',
57
+ 'Review frontend changes for component reuse and design-system compliance',
58
+ ],
59
+ 'bundle': 'tela-build',
60
+ 'artifacts': ['components'],
61
+ })
62
+ })
63
+
37
64
  it('appends package docs to the tela-build skill', () => {
38
65
  const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
39
66
  const layerPath = mkdtempSync(join(tmpdir(), 'tela-build-layer-'))
@@ -7,7 +7,8 @@ import { getTypeResolver } from './type-resolver'
7
7
  import type { TypeResolver } from './type-resolver'
8
8
  import { createVolarExtractor } from './extractors/volar-extract'
9
9
  import type { VolarExtractor } from './extractors/volar-extract'
10
- import { telaBuildSkill } from './tela-build-skill'
10
+ import { telaBuildSkill, telaBuildSkillPantry } from './tela-build-skill'
11
+ import type { SkillPantryMetadata } from './tela-build-skill'
11
12
 
12
13
  const colors = {
13
14
  gray: '\x1B[90m',
@@ -783,6 +784,7 @@ ${componentLinks.join('\n')}${docsSection}
783
784
  const skillMd = wrapWithSkillFrontmatter({
784
785
  name: telaBuildSkill.name,
785
786
  description: telaBuildSkill.description,
787
+ pantry: telaBuildSkillPantry,
786
788
  }, body)
787
789
  writeFileSync(join(skillDir, 'SKILL.md'), skillMd, 'utf-8')
788
790
  }
@@ -887,8 +889,8 @@ function toKebabFromTag(tagName: string): string {
887
889
  .replace(/^-+|-+$/g, '')
888
890
  }
889
891
 
890
- function wrapWithSkillFrontmatter(meta: { name: string, description: string, allowedTools?: string[] }, body: string): string {
891
- const { name, description, allowedTools } = meta
892
+ function wrapWithSkillFrontmatter(meta: { name: string, description: string, allowedTools?: string[], pantry?: SkillPantryMetadata }, body: string): string {
893
+ const { name, description, allowedTools, pantry } = meta
892
894
  const lines: string[] = []
893
895
  lines.push('---')
894
896
  lines.push(`name: ${sanitizeSkillName(name)}`)
@@ -896,6 +898,9 @@ function wrapWithSkillFrontmatter(meta: { name: string, description: string, all
896
898
  if (allowedTools && allowedTools.length > 0) {
897
899
  lines.push(`allowed-tools: ${allowedTools.join(', ')}`)
898
900
  }
901
+ if (pantry) {
902
+ lines.push(...renderPantryFrontmatter(pantry))
903
+ }
899
904
  lines.push('---')
900
905
  lines.push('')
901
906
  lines.push(body.trimStart())
@@ -918,6 +923,62 @@ function quoteFrontmatterString(value: string): string {
918
923
  return JSON.stringify(value)
919
924
  }
920
925
 
926
+ /**
927
+ * A YAML plain scalar, unless the value could be read as anything other than the
928
+ * string it is — then a double-quoted scalar. Quoting only what needs it keeps
929
+ * the generated frontmatter readable and stable.
930
+ */
931
+ function formatFrontmatterScalar(value: string): string {
932
+ return isPlainFrontmatterScalar(value) ? value : quoteFrontmatterString(value)
933
+ }
934
+
935
+ function isPlainFrontmatterScalar(value: string): boolean {
936
+ // Surrounding whitespace is not preserved by a plain scalar.
937
+ if (!value || value !== value.trim())
938
+ return false
939
+ // A leading indicator character starts a collection, comment, anchor, tag, or
940
+ // quoted/block scalar rather than a plain one.
941
+ if (/^[-?:,[\]{}#&*!|>'"%@`]/.test(value))
942
+ return false
943
+ // ": " opens a mapping, " #" opens a comment, and a trailing ":" makes a key.
944
+ if (value.includes(': ') || value.includes(' #') || value.endsWith(':'))
945
+ return false
946
+ if (/[\n\r\t]/.test(value))
947
+ return false
948
+ // These would round-trip as a boolean, null, or number instead of a string.
949
+ if (/^(?:true|false|null|yes|no|on|off|~)$/i.test(value))
950
+ return false
951
+ return Number.isNaN(Number(value))
952
+ }
953
+
954
+ /**
955
+ * Render the `pantry:` block. The key order is fixed rather than derived from
956
+ * object iteration so the output is byte-stable across regenerations.
957
+ */
958
+ function renderPantryFrontmatter(pantry: SkillPantryMetadata): string[] {
959
+ const lines: string[] = ['pantry:']
960
+
961
+ // An empty sequence would emit a bare key that parses as null, so omit it and
962
+ // let the consumer report a missing field instead of a malformed one.
963
+ const pushSequence = (key: string, values: readonly string[]): void => {
964
+ if (values.length === 0)
965
+ return
966
+ lines.push(` ${key}:`)
967
+ for (const value of values) {
968
+ lines.push(` - ${formatFrontmatterScalar(value)}`)
969
+ }
970
+ }
971
+
972
+ pushSequence('owner', pantry.owner)
973
+ lines.push(` status: ${formatFrontmatterScalar(pantry.status)}`)
974
+ pushSequence('shelves', pantry.shelves)
975
+ pushSequence('use-cases', pantry.useCases)
976
+ lines.push(` bundle: ${formatFrontmatterScalar(pantry.bundle)}`)
977
+ pushSequence('artifacts', pantry.artifacts)
978
+
979
+ return lines
980
+ }
981
+
921
982
  function toGroupSlugFromKebab(kebab: string): string {
922
983
  const parts = kebab.split('-').filter(Boolean)
923
984
  if (parts.length <= 2)
@@ -1,4 +1,45 @@
1
+ /**
2
+ * Pantry metadata for a generated skill, emitted as a nested `pantry:` block in
3
+ * the SKILL.md frontmatter.
4
+ *
5
+ * This is plain data on purpose: nothing here imports from Pantry or talks to it
6
+ * over the network. Generation stays entirely local, and a generator run that
7
+ * supplies no metadata produces exactly the frontmatter it always did.
8
+ */
9
+ export interface SkillPantryMetadata {
10
+ owner: readonly string[]
11
+ status: string
12
+ shelves: readonly string[]
13
+ useCases: readonly string[]
14
+ bundle: string
15
+ artifacts: readonly string[]
16
+ }
17
+
1
18
  export const telaBuildSkill = {
2
19
  name: 'tela-build',
3
20
  description: 'Use when working on any UI in this repository. Ask for component props, slots, variants, and usage examples. Always consult this Tela Build index BEFORE making any UI changes or creating new UI. Use it for components, layouts, styling, interactions, and design-token decisions.',
4
21
  } as const
22
+
23
+ /**
24
+ * Pantry catalog entry for the tela-build skill. It lives here rather than in
25
+ * `.agents/skills/tela-build/SKILL.md` because that file is generated: a block
26
+ * hand-written into the output is deleted by the next `generate-docs` run (and
27
+ * by every dev-server start, which regenerates through the Nuxt module).
28
+ *
29
+ * Shelf names are Title Case deliberately. Pantry derives shelf slugs from these
30
+ * strings, so lowercasing them would rename shelves shared with other
31
+ * ingredients across the whole catalog.
32
+ */
33
+ export const telaBuildSkillPantry = {
34
+ owner: ['guhrodrrigues'],
35
+ status: 'stable',
36
+ shelves: ['Frontend', 'Design Engineering', 'Developer Tools'],
37
+ useCases: [
38
+ 'Look up a Tela Build component\'s props, slots, variants, and usage examples before writing UI code',
39
+ 'Pick the correct design token for a color, text style, border, shadow, or spacing decision',
40
+ 'Polish an existing interface\'s radius, elevation, optical alignment, typography, and animations',
41
+ 'Review frontend changes for component reuse and design-system compliance',
42
+ ],
43
+ bundle: 'tela-build',
44
+ artifacts: ['components'],
45
+ } as const satisfies SkillPantryMetadata
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.62.1",
3
+ "version": "1.64.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",