@zvoove/unity-ui 2.21.0 → 2.22.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.
@@ -220,6 +220,98 @@ const SHADCN_MAP = {
220
220
  useClickOutside: null,
221
221
  };
222
222
 
223
+ // ─── MUI equivalence map ─────────────────────────────────────────────────────
224
+ const MUI_BASE = 'https://mui.com/material-ui/react-';
225
+ const MUI_X_BASE = 'https://mui.com/x/react-';
226
+
227
+ const MUI_MAP = {
228
+ Accordion: { name: 'Accordion', url: `${MUI_BASE}accordion/` },
229
+ ActionCard: null,
230
+ Avatar: { name: 'Avatar', url: `${MUI_BASE}avatar/` },
231
+ Badge: { name: 'Badge', url: `${MUI_BASE}badge/` },
232
+ Breadcrumbs: { name: 'Breadcrumbs', url: `${MUI_BASE}breadcrumbs/` },
233
+ Button: { name: 'Button', url: `${MUI_BASE}button/` },
234
+ Card: { name: 'Card', url: `${MUI_BASE}card/` },
235
+ ChatBubble: null,
236
+ Checkbox: { name: 'Checkbox', url: `${MUI_BASE}checkbox/` },
237
+ Chip: { name: 'Chip', url: `${MUI_BASE}chip/` },
238
+ CodeBlock: null,
239
+ ConfirmationCard: {
240
+ name: 'Dialog',
241
+ url: `${MUI_BASE}dialog/`,
242
+ note: 'Use a Dialog with confirmation actions. ConfirmationCard is an inline card, not a modal.',
243
+ },
244
+ ContentBlock: null,
245
+ DatePicker: {
246
+ name: 'DatePicker',
247
+ url: `${MUI_X_BASE}date-picker/`,
248
+ note: 'MUI X component — requires @mui/x-date-pickers.',
249
+ },
250
+ Dialog: { name: 'Dialog', url: `${MUI_BASE}dialog/` },
251
+ Divider: { name: 'Divider', url: `${MUI_BASE}divider/` },
252
+ Expandable: { name: 'Collapse', url: `${MUI_BASE}collapse/` },
253
+ FormLabel: { name: 'FormLabel', url: `${MUI_BASE}text-field/` },
254
+ Grid: { name: 'Grid', url: `${MUI_BASE}grid/` },
255
+ Icon: { name: 'SvgIcon', url: `${MUI_BASE}icons/` },
256
+ InfoBox: { name: 'Alert', url: `${MUI_BASE}alert/` },
257
+ MessageActions: null,
258
+ Pagination: { name: 'Pagination', url: `${MUI_BASE}pagination/` },
259
+ PopUpMenu: { name: 'Menu', url: `${MUI_BASE}menu/` },
260
+ ProgressIndicator: {
261
+ name: 'LinearProgress / CircularProgress',
262
+ url: `${MUI_BASE}progress/`,
263
+ },
264
+ Radio: { name: 'RadioGroup', url: `${MUI_BASE}radio-button/` },
265
+ ScoreCard: null,
266
+ Segment: {
267
+ name: 'ToggleButtonGroup',
268
+ url: `${MUI_BASE}toggle-button/`,
269
+ },
270
+ Select: { name: 'Select', url: `${MUI_BASE}select/` },
271
+ Sheet: {
272
+ name: 'Drawer',
273
+ url: `${MUI_BASE}drawer/`,
274
+ note: 'Use a temporary or persistent Drawer.',
275
+ },
276
+ SideNavigation: {
277
+ name: 'Drawer (persistent)',
278
+ url: `${MUI_BASE}drawer/`,
279
+ },
280
+ Skeleton: { name: 'Skeleton', url: `${MUI_BASE}skeleton/` },
281
+ Snackbar: { name: 'Snackbar', url: `${MUI_BASE}snackbar/` },
282
+ Stack: { name: 'Stack', url: `${MUI_BASE}stack/` },
283
+ Switch: { name: 'Switch', url: `${MUI_BASE}switch/` },
284
+ Table: {
285
+ name: 'Table / DataGrid',
286
+ url: `${MUI_BASE}table/`,
287
+ note: 'For advanced features use MUI X DataGrid (@mui/x-data-grid).',
288
+ },
289
+ Tabs: { name: 'Tabs', url: `${MUI_BASE}tabs/` },
290
+ Tag: {
291
+ name: 'Chip',
292
+ url: `${MUI_BASE}chip/`,
293
+ note: 'MUI Chip covers static labels; Unity UI Tag is display-only with more color/tone variants.',
294
+ },
295
+ TextField: { name: 'TextField', url: `${MUI_BASE}text-field/` },
296
+ Textarea: {
297
+ name: 'TextField (multiline)',
298
+ url: `${MUI_BASE}text-field/`,
299
+ note: 'Use TextField with multiline and rows props.',
300
+ },
301
+ Tooltip: { name: 'Tooltip', url: `${MUI_BASE}tooltip/` },
302
+ TopBar: { name: 'AppBar', url: `${MUI_BASE}app-bar/` },
303
+ Typography: { name: 'Typography', url: `${MUI_BASE}typography/` },
304
+ Uploader: null,
305
+ useBreakpoint: {
306
+ name: 'useMediaQuery',
307
+ url: 'https://mui.com/material-ui/react-use-media-query/',
308
+ },
309
+ useClickOutside: {
310
+ name: 'ClickAwayListener',
311
+ url: `${MUI_BASE}click-away-listener/`,
312
+ },
313
+ };
314
+
223
315
  // ─── Storybook documentation links ───────────────────────────────────────────
224
316
  // Maps component name → Storybook title path (as defined in *.stories.tsx).
225
317
  // URL: https://main--67c03f013fea08bb2f926e5f.chromatic.com/?path=/docs/<path>--docs
@@ -358,6 +450,8 @@ function detectSection(line) {
358
450
  '## RULES FOR AI AGENTS': 'rules',
359
451
  '## SPACING SCALE': 'spacing',
360
452
  '## ICON NAMES': 'icons',
453
+ '## STYLING': 'styling',
454
+ '## CUSTOM COMPONENT': 'customComponent',
361
455
  };
362
456
 
363
457
  for (const [prefix, key] of Object.entries(sectionHeaders)) {
@@ -394,6 +488,8 @@ function parseLlmsTxt(content) {
394
488
  responsive: [],
395
489
  rules: [],
396
490
  spacing: [],
491
+ styling: [],
492
+ customComponent: [],
397
493
  icons: [],
398
494
  example: [],
399
495
  };
@@ -415,6 +511,11 @@ function parseLlmsTxt(content) {
415
511
  }
416
512
 
417
513
  function handleComponentHeader(line) {
514
+ // In prose sections, ### sub-headings are content, not component names
515
+ if (activeSection === 'styling' || activeSection === 'customComponent') {
516
+ handleContentLine(line);
517
+ return;
518
+ }
418
519
  flush();
419
520
  currentComponent = line.slice(4).trim();
420
521
  if (activeSection !== 'icons') activeSection = null;
@@ -445,6 +546,8 @@ function parseLlmsTxt(content) {
445
546
  responsive: trimBucket('responsive'),
446
547
  rules: trimBucket('rules'),
447
548
  spacing: trimBucket('spacing'),
549
+ styling: trimBucket('styling'),
550
+ customComponent: trimBucket('customComponent'),
448
551
  icons: trimBucket('icons'),
449
552
  example: trimBucket('example'),
450
553
  components,
@@ -471,6 +574,7 @@ function generateComponentSkill(name, content) {
471
574
  if (shadcn) {
472
575
  description += ` Comparable to shadcn/ui ${shadcn.name}.`;
473
576
  }
577
+ description += String.raw`\nTRIGGER when: user needs ${name} in a project using @zvoove/unity-ui.\nDO NOT read dist/ or source files — all API docs are in this skill.`;
474
578
 
475
579
  const frontmatter = [
476
580
  '---',
@@ -577,14 +681,21 @@ function generateIndexSkill(componentNames) {
577
681
  `| ${n} | [${SHADCN_MAP[n].name}](${SHADCN_MAP[n].url}) | \`unity-ui-${kebabCase(n)}\` |`
578
682
  );
579
683
 
684
+ const muiRows = componentNames
685
+ .filter((n) => MUI_MAP[n])
686
+ .map(
687
+ (n) =>
688
+ `| ${n} | [${MUI_MAP[n].name}](${MUI_MAP[n].url}) | \`unity-ui-${kebabCase(n)}\` |`
689
+ );
690
+
580
691
  const uniqueRows = componentNames
581
- .filter((n) => !SHADCN_MAP[n])
582
- .map((n) => `| ${n} | — | \`unity-ui-${kebabCase(n)}\` |`);
692
+ .filter((n) => !SHADCN_MAP[n] && !MUI_MAP[n])
693
+ .map((n) => `| ${n} | \`unity-ui-${kebabCase(n)}\` |`);
583
694
 
584
695
  const frontmatter = [
585
696
  '---',
586
697
  'name: unity-ui-index',
587
- 'description: "Component index for Unity UI (@zvoove/unity-ui). Maps all 40+ components to their shadcn/ui equivalents. Use this to find the right component."',
698
+ 'description: "Component index for Unity UI (@zvoove/unity-ui). Maps all 40+ components to their shadcn/ui and MUI equivalents. Use this to find the right component or migrate from another library."',
588
699
  'category: index',
589
700
  '---',
590
701
  ].join('\n');
@@ -595,17 +706,23 @@ function generateIndexSkill(componentNames) {
595
706
 
596
707
  > Use this index to find the right Unity UI component. Each component has its own skill with full documentation.
597
708
 
598
- ## Components with shadcn/ui Equivalents
709
+ ## shadcn/ui → Unity UI
599
710
 
600
711
  | Unity UI | shadcn/ui | Skill |
601
712
  |----------|-----------|-------|
602
713
  ${shadcnRows.join('\n')}
603
714
 
715
+ ## MUI → Unity UI
716
+
717
+ | Unity UI | MUI | Skill |
718
+ |----------|-----|-------|
719
+ ${muiRows.join('\n')}
720
+
604
721
  ## Unity UI–Only Components
605
722
 
606
- | Unity UI | shadcn/ui | Skill |
607
- |----------|-----------|-------|
608
- ${uniqueRows.join('\n')}
723
+ | Unity UI | Skill |
724
+ |----------|-------|
725
+ ${uniqueRows.map((r) => r.replace(' | — ', '')).join('\n')}
609
726
 
610
727
  ## How to Use Skills
611
728
 
@@ -615,6 +732,1074 @@ AI agents should load the relevant skill when they need to use a specific compon
615
732
  );
616
733
  }
617
734
 
735
+ function generateStylingSkill(parsed) {
736
+ const description = [
737
+ 'Styling guide for Unity UI (@zvoove/unity-ui). Tailwind CSS v4 + design tokens + tailwind-variants.',
738
+ 'TRIGGER when: user needs to style a custom element, asks about CSS/styling, or produces inline styles / arbitrary Tailwind values in a @zvoove/unity-ui project.',
739
+ 'DO NOT use inline styles, arbitrary Tailwind values, or raw CSS — use design tokens and tv() instead.',
740
+ ].join(String.raw`\n`);
741
+
742
+ const frontmatter = [
743
+ '---',
744
+ 'name: unity-ui-styling',
745
+ `description: "${description}"`,
746
+ 'category: styling',
747
+ '---',
748
+ ].join('\n');
749
+
750
+ return (
751
+ frontmatter + `\n\n# Unity UI — Styling Guide\n\n` + parsed.styling + '\n'
752
+ );
753
+ }
754
+
755
+ function generateCustomComponentSkill(parsed) {
756
+ const description = [
757
+ 'Guide for creating a custom component in a project using @zvoove/unity-ui, following the same conventions as the design system.',
758
+ 'TRIGGER when: user needs to create a custom component that does not exist in @zvoove/unity-ui, following Unity UI patterns.',
759
+ 'DO NOT TRIGGER when: an existing Unity UI component already covers the use case — use that instead.',
760
+ ].join(String.raw`\n`);
761
+
762
+ const frontmatter = [
763
+ '---',
764
+ 'name: unity-ui-custom-component',
765
+ `description: "${description}"`,
766
+ 'category: authoring',
767
+ '---',
768
+ ].join('\n');
769
+
770
+ return (
771
+ frontmatter +
772
+ `\n\n# Unity UI — Creating a Custom Component\n\n` +
773
+ parsed.customComponent +
774
+ '\n'
775
+ );
776
+ }
777
+
778
+ function generateFormsSkill() {
779
+ const description = [
780
+ 'Form composition guide for @zvoove/unity-ui with react-hook-form and zod.',
781
+ 'TRIGGER when: user is building a form, adding validation, wiring inputs to a form library, or asking how to use Unity UI inputs with react-hook-form.',
782
+ 'DO NOT use register() — Unity UI inputs are controlled components; always use Controller.',
783
+ ].join(String.raw`\n`);
784
+
785
+ const frontmatter = [
786
+ '---',
787
+ 'name: unity-ui-forms',
788
+ `description: "${description}"`,
789
+ 'category: forms',
790
+ '---',
791
+ ].join('\n');
792
+
793
+ return (
794
+ frontmatter +
795
+ `\n\n# Unity UI — Form Composition
796
+
797
+ > Unity UI inputs are **controlled components**. Use \`Controller\` from react-hook-form — never \`register()\`.
798
+
799
+ ## Installing
800
+
801
+ \`\`\`bash
802
+ npm install react-hook-form zod @hookform/resolvers
803
+ \`\`\`
804
+
805
+ ## Basic Pattern
806
+
807
+ \`\`\`tsx
808
+ import { useForm, Controller } from 'react-hook-form';
809
+ import { zodResolver } from '@hookform/resolvers/zod';
810
+ import { z } from 'zod';
811
+ import { Button, Select, Stack, TextField } from '@zvoove/unity-ui';
812
+
813
+ const schema = z.object({
814
+ email: z.string().email('Ungültige E-Mail-Adresse'),
815
+ role: z.string().min(1, 'Pflichtfeld'),
816
+ });
817
+
818
+ type FormValues = z.infer<typeof schema>;
819
+
820
+ export function ExampleForm() {
821
+ const { control, handleSubmit } = useForm<FormValues>({
822
+ resolver: zodResolver(schema),
823
+ defaultValues: { email: '', role: '' },
824
+ });
825
+
826
+ return (
827
+ <form onSubmit={handleSubmit((data) => console.log(data))}>
828
+ <Stack direction="column" gap="md">
829
+ <Controller
830
+ name="email"
831
+ control={control}
832
+ render={({ field, fieldState }) => (
833
+ <TextField
834
+ {...field}
835
+ label="E-Mail"
836
+ error={!!fieldState.error}
837
+ errorMessage={fieldState.error?.message}
838
+ />
839
+ )}
840
+ />
841
+ <Controller
842
+ name="role"
843
+ control={control}
844
+ render={({ field, fieldState }) => (
845
+ <Select
846
+ {...field}
847
+ label="Rolle"
848
+ options={[
849
+ { value: 'admin', label: 'Administrator' },
850
+ { value: 'user', label: 'Benutzer' },
851
+ ]}
852
+ error={!!fieldState.error}
853
+ errorMessage={fieldState.error?.message}
854
+ />
855
+ )}
856
+ />
857
+ <Stack direction="row" gap="sm" justify="flex-end">
858
+ <Button type="submit" variant="filled">Speichern</Button>
859
+ </Stack>
860
+ </Stack>
861
+ </form>
862
+ );
863
+ }
864
+ \`\`\`
865
+
866
+ ## Input Binding Reference
867
+
868
+ | Component | Binding pattern | Notes |
869
+ |-----------|----------------|-------|
870
+ | TextField | \`{...field}\` | value + onChange + onBlur + name |
871
+ | Textarea | \`{...field}\` | same as TextField |
872
+ | Select | \`{...field}\` | value + onChange |
873
+ | Checkbox | \`checked={field.value}\` | use \`checked\`, not \`value\` |
874
+ | Switch | \`checked={field.value}\` | use \`checked\`, not \`value\` |
875
+ | DatePicker | \`value={field.value} onChange={field.onChange}\` | pass date value directly |
876
+ | Radio (RadioGroup) | \`value={field.value} onChange={field.onChange}\` | — |
877
+
878
+ ## Checkbox / Switch Pattern
879
+
880
+ \`\`\`tsx
881
+ <Controller
882
+ name="acceptTerms"
883
+ control={control}
884
+ render={({ field }) => (
885
+ <Checkbox
886
+ label="Ich akzeptiere die Nutzungsbedingungen"
887
+ checked={field.value ?? false}
888
+ onChange={(e) => field.onChange(e.target.checked)}
889
+ />
890
+ )}
891
+ />
892
+ \`\`\`
893
+
894
+ ## DatePicker Pattern
895
+
896
+ \`\`\`tsx
897
+ <Controller
898
+ name="birthDate"
899
+ control={control}
900
+ render={({ field, fieldState }) => (
901
+ <DatePicker
902
+ label="Geburtsdatum"
903
+ value={field.value}
904
+ onChange={field.onChange}
905
+ error={!!fieldState.error}
906
+ errorMessage={fieldState.error?.message}
907
+ />
908
+ )}
909
+ />
910
+ \`\`\`
911
+
912
+ ## Complete Login Form
913
+
914
+ \`\`\`tsx
915
+ import { useForm, Controller } from 'react-hook-form';
916
+ import { zodResolver } from '@hookform/resolvers/zod';
917
+ import { z } from 'zod';
918
+ import { Button, Card, Checkbox, Stack, TextField, Typography } from '@zvoove/unity-ui';
919
+
920
+ const loginSchema = z.object({
921
+ email: z.string().email('Ungültige E-Mail-Adresse'),
922
+ password: z.string().min(8, 'Mindestens 8 Zeichen erforderlich'),
923
+ rememberMe: z.boolean().optional(),
924
+ });
925
+
926
+ type LoginValues = z.infer<typeof loginSchema>;
927
+
928
+ export function LoginForm() {
929
+ const {
930
+ control,
931
+ handleSubmit,
932
+ formState: { isSubmitting },
933
+ } = useForm<LoginValues>({
934
+ resolver: zodResolver(loginSchema),
935
+ defaultValues: { email: '', password: '', rememberMe: false },
936
+ });
937
+
938
+ const onSubmit = async (data: LoginValues) => {
939
+ await login(data);
940
+ };
941
+
942
+ return (
943
+ <Card padding="xl">
944
+ <form onSubmit={handleSubmit(onSubmit)}>
945
+ <Stack direction="column" gap="md">
946
+ <Typography variant="title-large">Anmelden</Typography>
947
+
948
+ <Controller
949
+ name="email"
950
+ control={control}
951
+ render={({ field, fieldState }) => (
952
+ <TextField
953
+ {...field}
954
+ label="E-Mail"
955
+ type="email"
956
+ error={!!fieldState.error}
957
+ errorMessage={fieldState.error?.message}
958
+ />
959
+ )}
960
+ />
961
+
962
+ <Controller
963
+ name="password"
964
+ control={control}
965
+ render={({ field, fieldState }) => (
966
+ <TextField
967
+ {...field}
968
+ label="Passwort"
969
+ type="password"
970
+ error={!!fieldState.error}
971
+ errorMessage={fieldState.error?.message}
972
+ />
973
+ )}
974
+ />
975
+
976
+ <Controller
977
+ name="rememberMe"
978
+ control={control}
979
+ render={({ field }) => (
980
+ <Checkbox
981
+ label="Angemeldet bleiben"
982
+ checked={field.value ?? false}
983
+ onChange={(e) => field.onChange(e.target.checked)}
984
+ />
985
+ )}
986
+ />
987
+
988
+ <Stack direction="row" justify="flex-end">
989
+ <Button type="submit" variant="filled" disabled={isSubmitting}>
990
+ {isSubmitting ? 'Wird angemeldet…' : 'Anmelden'}
991
+ </Button>
992
+ </Stack>
993
+ </Stack>
994
+ </form>
995
+ </Card>
996
+ );
997
+ }
998
+ \`\`\`
999
+
1000
+ ## Rules
1001
+
1002
+ - NEVER use \`register()\` — Unity UI inputs are controlled components; always use \`Controller\`
1003
+ - ALWAYS use \`zodResolver\` from \`@hookform/resolvers/zod\` for schema validation
1004
+ - Checkbox and Switch require \`checked={field.value}\` — not \`value\`
1005
+ - Display errors with \`error={!!fieldState.error}\` + \`errorMessage={fieldState.error?.message}\`
1006
+ - Use \`Stack\` for form layout — never raw \`<div>\` with flexbox CSS
1007
+ - Default texts, labels, and validation messages must be in German
1008
+ `
1009
+ );
1010
+ }
1011
+
1012
+ function generateLayoutsSkill() {
1013
+ const description = [
1014
+ 'Common app layout patterns for @zvoove/unity-ui: app shells, dashboards, settings pages, master/detail.',
1015
+ 'TRIGGER when: user is building a page layout, app shell, or multi-section UI in a @zvoove/unity-ui project.',
1016
+ 'DO NOT use raw div + CSS for layout — always use Stack, Grid, TopBar, SideNavigation.',
1017
+ ].join(String.raw`\n`);
1018
+
1019
+ const frontmatter = [
1020
+ '---',
1021
+ 'name: unity-ui-layouts',
1022
+ `description: "${description}"`,
1023
+ 'category: layout',
1024
+ '---',
1025
+ ].join('\n');
1026
+
1027
+ return (
1028
+ frontmatter +
1029
+ `\n\n# Unity UI — Layout Patterns
1030
+
1031
+ > Use \`Stack\` and \`Grid\` for all layout. Never use raw \`<div style={{ display: 'flex' }}>\`.
1032
+
1033
+ ## App Shell (TopBar + SideNavigation)
1034
+
1035
+ \`\`\`tsx
1036
+ import { Avatar, SideNavigation, Stack, TopBar } from '@zvoove/unity-ui';
1037
+
1038
+ export function AppShell({ children }: { children: React.ReactNode }) {
1039
+ const [activeId, setActiveId] = useState('dashboard');
1040
+
1041
+ return (
1042
+ <Stack direction="column" style={{ height: '100vh' }}>
1043
+ <TopBar
1044
+ title="Meine App"
1045
+ actions={<Avatar initials="AB" />}
1046
+ />
1047
+ <Stack direction="row" style={{ flex: 1, overflow: 'hidden' }}>
1048
+ <SideNavigation
1049
+ items={[
1050
+ { id: 'dashboard', label: 'Dashboard', icon: 'House' },
1051
+ { id: 'users', label: 'Benutzer', icon: 'Users' },
1052
+ { id: 'settings', label: 'Einstellungen', icon: 'Gear' },
1053
+ ]}
1054
+ activeId={activeId}
1055
+ onSelect={setActiveId}
1056
+ />
1057
+ <Stack
1058
+ direction="column"
1059
+ padding="xl"
1060
+ style={{ flex: 1, overflow: 'auto' }}
1061
+ >
1062
+ {children}
1063
+ </Stack>
1064
+ </Stack>
1065
+ </Stack>
1066
+ );
1067
+ }
1068
+ \`\`\`
1069
+
1070
+ ## Dashboard Layout
1071
+
1072
+ \`\`\`tsx
1073
+ <Stack direction="column" gap="lg" padding="xl">
1074
+ <Typography variant="headline-small">Dashboard</Typography>
1075
+
1076
+ {/* KPI summary row */}
1077
+ <Grid columns={{ mobile: 1, tablet: 2, desktop: 4 }} gap="md">
1078
+ {kpis.map((kpi) => (
1079
+ <Card key={kpi.id} padding="md">
1080
+ <Stack direction="column" gap="xs">
1081
+ <Typography variant="label-medium">{kpi.label}</Typography>
1082
+ <Typography variant="display-small">{kpi.value}</Typography>
1083
+ </Stack>
1084
+ </Card>
1085
+ ))}
1086
+ </Grid>
1087
+
1088
+ {/* Main area: 2/3 table + 1/3 sidebar */}
1089
+ <Grid columns={{ mobile: 1, laptop: 3 }} gap="lg">
1090
+ <Stack style={{ gridColumn: 'span 2' }}>
1091
+ <Card padding="md">
1092
+ <Table columns={columns} rows={rows} />
1093
+ </Card>
1094
+ </Stack>
1095
+ <Stack direction="column" gap="md">
1096
+ <Card padding="md">
1097
+ <Typography variant="title-medium">Aktivität</Typography>
1098
+ {/* activity list */}
1099
+ </Card>
1100
+ </Stack>
1101
+ </Grid>
1102
+ </Stack>
1103
+ \`\`\`
1104
+
1105
+ ## Settings Page (Two-Column)
1106
+
1107
+ \`\`\`tsx
1108
+ <Stack direction="column" gap="xl" padding="xl">
1109
+ <Typography variant="headline-small">Einstellungen</Typography>
1110
+
1111
+ <Stack direction={{ mobile: 'column', tablet: 'row' }} gap="xl">
1112
+ {/* Left: section label */}
1113
+ <Stack direction="column" gap="xs" style={{ minWidth: 200 }}>
1114
+ <Typography variant="title-medium">Profil</Typography>
1115
+ <Typography variant="body-medium">Persönliche Informationen.</Typography>
1116
+ </Stack>
1117
+
1118
+ {/* Right: fields */}
1119
+ <Card padding="lg" style={{ flex: 1 }}>
1120
+ <Stack direction="column" gap="md">
1121
+ <Stack direction={{ mobile: 'column', tablet: 'row' }} gap="md">
1122
+ <TextField label="Vorname" value={firstName} onChange={setFirstName} />
1123
+ <TextField label="Nachname" value={lastName} onChange={setLastName} />
1124
+ </Stack>
1125
+ <TextField label="E-Mail" value={email} onChange={setEmail} />
1126
+ <Switch
1127
+ label="E-Mail-Benachrichtigungen aktivieren"
1128
+ checked={notifications}
1129
+ onChange={(e) => setNotifications(e.target.checked)}
1130
+ />
1131
+ </Stack>
1132
+ </Card>
1133
+ </Stack>
1134
+
1135
+ {/* Sticky action bar */}
1136
+ <Stack direction="row" gap="sm" justify="flex-end">
1137
+ <Button variant="outlined">Abbrechen</Button>
1138
+ <Button variant="filled">Speichern</Button>
1139
+ </Stack>
1140
+ </Stack>
1141
+ \`\`\`
1142
+
1143
+ ## Master / Detail (List + Detail)
1144
+
1145
+ \`\`\`tsx
1146
+ <Stack
1147
+ direction={{ mobile: 'column', tablet: 'row' }}
1148
+ gap="md"
1149
+ padding="xl"
1150
+ style={{ height: '100%' }}
1151
+ >
1152
+ {/* List panel */}
1153
+ <Stack direction="column" gap="sm" style={{ width: 300, flexShrink: 0 }}>
1154
+ {items.map((item) => (
1155
+ <Card
1156
+ key={item.id}
1157
+ padding="md"
1158
+ onClick={() => setSelected(item.id)}
1159
+ >
1160
+ <Typography variant="body-medium">{item.title}</Typography>
1161
+ </Card>
1162
+ ))}
1163
+ </Stack>
1164
+
1165
+ {/* Detail panel */}
1166
+ <Stack direction="column" gap="md" style={{ flex: 1 }}>
1167
+ {selected ? (
1168
+ <Card padding="lg">{/* detail content */}</Card>
1169
+ ) : (
1170
+ <Stack direction="column" align="center" justify="center" style={{ height: '100%' }}>
1171
+ <Typography variant="body-medium">Wählen Sie einen Eintrag aus.</Typography>
1172
+ </Stack>
1173
+ )}
1174
+ </Stack>
1175
+ </Stack>
1176
+ \`\`\`
1177
+
1178
+ ## Centered Auth / Empty State
1179
+
1180
+ \`\`\`tsx
1181
+ <Stack
1182
+ direction="column"
1183
+ align="center"
1184
+ justify="center"
1185
+ style={{ minHeight: '100vh' }}
1186
+ gap="lg"
1187
+ >
1188
+ <Card padding="xl" style={{ width: '100%', maxWidth: 400 }}>
1189
+ <Stack direction="column" gap="md">
1190
+ <Typography variant="headline-small">Anmelden</Typography>
1191
+ {/* form fields */}
1192
+ </Stack>
1193
+ </Card>
1194
+ </Stack>
1195
+ \`\`\`
1196
+
1197
+ ## Card Grid (Content Listing)
1198
+
1199
+ \`\`\`tsx
1200
+ <Stack direction="column" gap="lg" padding="xl">
1201
+ <Stack direction="row" justify="space-between" align="center">
1202
+ <Typography variant="headline-small">Projekte</Typography>
1203
+ <Button variant="filled" icon="Plus">Neues Projekt</Button>
1204
+ </Stack>
1205
+
1206
+ <Grid columns={{ mobile: 1, tablet: 2, desktop: 3 }} gap="md">
1207
+ {projects.map((project) => (
1208
+ <Card key={project.id} padding="md">
1209
+ <Stack direction="column" gap="sm">
1210
+ <Typography variant="title-medium">{project.name}</Typography>
1211
+ <Typography variant="body-small">{project.description}</Typography>
1212
+ <Stack direction="row" gap="xs" justify="flex-end">
1213
+ <Button variant="ghost" size="sm">Bearbeiten</Button>
1214
+ </Stack>
1215
+ </Stack>
1216
+ </Card>
1217
+ ))}
1218
+ </Grid>
1219
+ </Stack>
1220
+ \`\`\`
1221
+
1222
+ ## Rules
1223
+
1224
+ - Use \`Stack\` and \`Grid\` for ALL layout — never \`<div style={{ display: 'flex' }}>\`
1225
+ - Use \`TopBar\` + \`SideNavigation\` for app shells — never custom nav bars
1226
+ - Use \`Card\` for content containers — never styled divs
1227
+ - Use spacing tokens for gap/padding — never arbitrary pixel values (\`gap-lg\` not \`gap-5\`)
1228
+ - Always make layouts responsive with \`ResponsiveType\` props
1229
+ - SideNavigation handles its own responsive collapse — do not wrap it in conditional rendering
1230
+ `
1231
+ );
1232
+ }
1233
+
1234
+ function generateDarkModeSkill() {
1235
+ const description = [
1236
+ 'Dark mode setup and theme toggling for @zvoove/unity-ui projects.',
1237
+ 'TRIGGER when: user wants to add dark mode, a theme toggle, or asks how Unity UI dark mode works.',
1238
+ 'DO NOT use prefers-color-scheme media queries or className=\\"dark\\" — Unity UI uses data-theme=\\"dark\\".',
1239
+ ].join(String.raw`\n`);
1240
+
1241
+ const frontmatter = [
1242
+ '---',
1243
+ 'name: unity-ui-dark-mode',
1244
+ `description: "${description}"`,
1245
+ 'category: theming',
1246
+ '---',
1247
+ ].join('\n');
1248
+
1249
+ return (
1250
+ frontmatter +
1251
+ `\n\n# Unity UI — Dark Mode
1252
+
1253
+ > Dark mode is activated by \`data-theme="dark"\` on any ancestor element.
1254
+ > All semantic design tokens adapt automatically — no per-component changes needed.
1255
+
1256
+ ## How It Works
1257
+
1258
+ \`\`\`html
1259
+ <!-- Light mode (default) -->
1260
+ <div>...</div>
1261
+
1262
+ <!-- Dark mode — applies to this element and all descendants -->
1263
+ <div data-theme="dark">...</div>
1264
+ \`\`\`
1265
+
1266
+ Unity UI uses Tailwind's **selector strategy** (\`[data-theme="dark"]\`), not the class or media-query strategy.
1267
+ Configure Tailwind accordingly if you need \`dark:\` in custom components:
1268
+
1269
+ \`\`\`ts
1270
+ // tailwind.config.ts
1271
+ export default {
1272
+ darkMode: ['selector', '[data-theme="dark"]'],
1273
+ };
1274
+ \`\`\`
1275
+
1276
+ ## React ThemeProvider
1277
+
1278
+ \`\`\`tsx
1279
+ import { createContext, useContext, useEffect, useState } from 'react';
1280
+
1281
+ type Theme = 'light' | 'dark';
1282
+
1283
+ const ThemeContext = createContext<{
1284
+ theme: Theme;
1285
+ toggleTheme: () => void;
1286
+ }>({ theme: 'light', toggleTheme: () => {} });
1287
+
1288
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
1289
+ const [theme, setTheme] = useState<Theme>(() => {
1290
+ const stored = localStorage.getItem('theme') as Theme | null;
1291
+ if (stored) return stored;
1292
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
1293
+ });
1294
+
1295
+ // Sync with OS preference changes (only when no stored preference)
1296
+ useEffect(() => {
1297
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
1298
+ const handler = (e: MediaQueryListEvent) => {
1299
+ if (!localStorage.getItem('theme')) {
1300
+ setTheme(e.matches ? 'dark' : 'light');
1301
+ }
1302
+ };
1303
+ mq.addEventListener('change', handler);
1304
+ return () => mq.removeEventListener('change', handler);
1305
+ }, []);
1306
+
1307
+ const toggleTheme = () => {
1308
+ setTheme((t) => {
1309
+ const next = t === 'light' ? 'dark' : 'light';
1310
+ localStorage.setItem('theme', next);
1311
+ return next;
1312
+ });
1313
+ };
1314
+
1315
+ return (
1316
+ <ThemeContext.Provider value={{ theme, toggleTheme }}>
1317
+ <div data-theme={theme} style={{ minHeight: '100vh' }}>
1318
+ {children}
1319
+ </div>
1320
+ </ThemeContext.Provider>
1321
+ );
1322
+ }
1323
+
1324
+ export const useTheme = () => useContext(ThemeContext);
1325
+ \`\`\`
1326
+
1327
+ Wrap the app root:
1328
+
1329
+ \`\`\`tsx
1330
+ // main.tsx
1331
+ <ThemeProvider>
1332
+ <App />
1333
+ </ThemeProvider>
1334
+ \`\`\`
1335
+
1336
+ ## Theme Toggle Button
1337
+
1338
+ \`\`\`tsx
1339
+ import { Button } from '@zvoove/unity-ui';
1340
+ import { useTheme } from './ThemeProvider';
1341
+
1342
+ export function ThemeToggle() {
1343
+ const { theme, toggleTheme } = useTheme();
1344
+
1345
+ return (
1346
+ <Button
1347
+ variant="ghost"
1348
+ icon={theme === 'dark' ? 'Sun' : 'Moon'}
1349
+ onClick={toggleTheme}
1350
+ aria-label={
1351
+ theme === 'dark'
1352
+ ? 'Zum hellen Modus wechseln'
1353
+ : 'Zum dunklen Modus wechseln'
1354
+ }
1355
+ />
1356
+ );
1357
+ }
1358
+ \`\`\`
1359
+
1360
+ Place it in \`TopBar\`'s \`actions\` prop:
1361
+
1362
+ \`\`\`tsx
1363
+ <TopBar title="Meine App" actions={<ThemeToggle />} />
1364
+ \`\`\`
1365
+
1366
+ ## Token Behaviour in Dark Mode
1367
+
1368
+ Semantic tokens adapt automatically — no \`dark:\` prefix needed for these:
1369
+
1370
+ | Token | Light | Dark |
1371
+ |-------|-------|------|
1372
+ | \`bg-surface\` | white / light gray | dark gray |
1373
+ | \`bg-primary\` | brand color | lighter brand color |
1374
+ | \`text-on-surface\` | near-black | near-white |
1375
+ | \`text-on-surface-variant\` | gray | lighter gray |
1376
+ | \`border-outline\` | gray border | darker border |
1377
+ | \`bg-background\` | page background | dark page background |
1378
+
1379
+ Only use \`dark:\` for values **outside** the token system (custom hex colors, images, etc.).
1380
+
1381
+ ## Storybook Dark Mode
1382
+
1383
+ \`\`\`tsx
1384
+ // In a story file
1385
+ export const DarkMode: Story = {
1386
+ decorators: [
1387
+ (Story) => (
1388
+ <div data-theme="dark" style={{ padding: '1rem', background: 'var(--color-background)' }}>
1389
+ <Story />
1390
+ </div>
1391
+ ),
1392
+ ],
1393
+ };
1394
+ \`\`\`
1395
+
1396
+ Or use the Storybook backgrounds addon — set the decorator on the global level in \`preview.ts\`.
1397
+
1398
+ ## Rules
1399
+
1400
+ - NEVER use \`prefers-color-scheme\` CSS to apply dark mode — use \`data-theme="dark"\`
1401
+ - NEVER use \`className="dark"\` — Unity UI uses the selector strategy, not the class strategy
1402
+ - Semantic tokens adapt automatically — only add \`dark:\` for custom non-token values
1403
+ - Persist theme in \`localStorage\` to avoid a flash-of-wrong-theme on reload
1404
+ - Do not apply \`data-theme\` to \`<html>\` or \`<body>\` unless your React root is there
1405
+ `
1406
+ );
1407
+ }
1408
+
1409
+ function generateMigrateShadcnSkill(componentNames) {
1410
+ const description = [
1411
+ 'Step-by-step guide for migrating a shadcn/ui codebase to @zvoove/unity-ui.',
1412
+ 'TRIGGER when: user wants to migrate from shadcn/ui to Unity UI, or is converting shadcn/ui components.',
1413
+ 'DO NOT keep shadcn/ui imports alongside Unity UI — replace fully.',
1414
+ ].join(String.raw`\n`);
1415
+
1416
+ const frontmatter = [
1417
+ '---',
1418
+ 'name: unity-ui-migrate-shadcn',
1419
+ `description: "${description}"`,
1420
+ 'category: migration',
1421
+ '---',
1422
+ ].join('\n');
1423
+
1424
+ const componentRows = componentNames
1425
+ .filter((n) => SHADCN_MAP[n])
1426
+ .map((n) => {
1427
+ const s = SHADCN_MAP[n];
1428
+ const note = s.note ? ` _(${s.note})_` : '';
1429
+ return `| [${s.name}](${s.url}) | \`<${n} />\` | \`unity-ui-${kebabCase(n)}\`${note} |`;
1430
+ });
1431
+
1432
+ const noEquivalent = componentNames
1433
+ .filter((n) => !SHADCN_MAP[n])
1434
+ .map(
1435
+ (n) => `- **${n}** — load skill \`unity-ui-${kebabCase(n)}\` for usage`
1436
+ )
1437
+ .join('\n');
1438
+
1439
+ return (
1440
+ frontmatter +
1441
+ `\n\n# Migrating from shadcn/ui to Unity UI
1442
+
1443
+ ## Step-by-Step Process
1444
+
1445
+ 1. **Install Unity UI** — see \`unity-ui-setup\` skill
1446
+ 2. **Run a global find** for \`from "@/components/ui/\` and \`from "~/components/ui/\` — those are shadcn/ui imports
1447
+ 3. **Replace each component** using the table below — load the \`unity-ui-<component>\` skill for full props
1448
+ 4. **Replace styling** — remove Tailwind class-heavy JSX and use Unity UI props instead
1449
+ 5. **Remove shadcn/ui** — delete the \`components/ui/\` folder and uninstall \`class-variance-authority\`, \`cmdk\`, \`@radix-ui\` etc.
1450
+
1451
+ ## Component Map
1452
+
1453
+ | shadcn/ui | Unity UI | Skill |
1454
+ |-----------|----------|-------|
1455
+ ${componentRows.join('\n')}
1456
+
1457
+ ## Unity UI–Only Components (no shadcn/ui equivalent)
1458
+
1459
+ ${noEquivalent}
1460
+
1461
+ ## Common Prop Differences
1462
+
1463
+ | Pattern | shadcn/ui | Unity UI |
1464
+ |---------|-----------|----------|
1465
+ | Text input | \`<Input />\` | \`<TextField label="..." />\` |
1466
+ | Validation error | custom error \`<p>\` | \`error={true} errorMessage="..."\` |
1467
+ | Button icon | wrap with \`<Icon />\` | \`icon="IconName"\` prop |
1468
+ | Loading state | manual spinner | \`loading={true}\` prop (Button) |
1469
+ | Form field wrapper | \`<FormField>\` + \`<FormControl>\` | direct Controller render |
1470
+ | Toast | \`toast("msg")\` from sonner | \`addSnackbar("msg")\` from useSnackbar |
1471
+ | Sheet position | \`side="right"\` | \`anchor="right"\` |
1472
+
1473
+ ## Removing shadcn/ui
1474
+
1475
+ \`\`\`bash
1476
+ # Remove shadcn/ui dependencies
1477
+ npm uninstall @radix-ui/react-* class-variance-authority cmdk lucide-react
1478
+
1479
+ # Delete the generated component folder
1480
+ rm -rf src/components/ui
1481
+ \`\`\`
1482
+
1483
+ ## Rules
1484
+
1485
+ - NEVER keep shadcn/ui and Unity UI components side-by-side — migrate fully
1486
+ - Replace all \`cn()\` usage in component files with \`twMerge()\` from tailwind-merge
1487
+ - shadcn/ui uses \`className\` extensively — Unity UI uses props (variant, size, etc.); remove the class overrides
1488
+ - Import everything from \`@zvoove/unity-ui\`, never from local \`components/ui/\`
1489
+ `
1490
+ );
1491
+ }
1492
+
1493
+ function generateMigrateMuiSkill(componentNames) {
1494
+ const description = [
1495
+ 'Step-by-step guide for migrating a Material UI (MUI) codebase to @zvoove/unity-ui.',
1496
+ 'TRIGGER when: user wants to migrate from MUI (@mui/material) to Unity UI, or is converting MUI components.',
1497
+ 'DO NOT keep MUI imports alongside Unity UI — replace fully.',
1498
+ ].join(String.raw`\n`);
1499
+
1500
+ const frontmatter = [
1501
+ '---',
1502
+ 'name: unity-ui-migrate-mui',
1503
+ `description: "${description}"`,
1504
+ 'category: migration',
1505
+ '---',
1506
+ ].join('\n');
1507
+
1508
+ const componentRows = componentNames
1509
+ .filter((n) => MUI_MAP[n])
1510
+ .map((n) => {
1511
+ const m = MUI_MAP[n];
1512
+ const note = m.note ? ` _(${m.note})_` : '';
1513
+ return `| [${m.name}](${m.url}) | \`<${n} />\` | \`unity-ui-${kebabCase(n)}\`${note} |`;
1514
+ });
1515
+
1516
+ const noEquivalent = componentNames
1517
+ .filter((n) => !MUI_MAP[n])
1518
+ .map(
1519
+ (n) => `- **${n}** — load skill \`unity-ui-${kebabCase(n)}\` for usage`
1520
+ )
1521
+ .join('\n');
1522
+
1523
+ return (
1524
+ frontmatter +
1525
+ `\n\n# Migrating from MUI to Unity UI
1526
+
1527
+ ## Step-by-Step Process
1528
+
1529
+ 1. **Install Unity UI** — see \`unity-ui-setup\` skill
1530
+ 2. **Run a global find** for \`from "@mui/material\` and \`from "@mui/x-\` — those are MUI imports
1531
+ 3. **Replace each component** using the table below — load the \`unity-ui-<component>\` skill for full props
1532
+ 4. **Replace the sx prop** — MUI's \`sx\` system does not exist in Unity UI; use Tailwind tokens instead (load \`unity-ui-styling\`)
1533
+ 5. **Remove ThemeProvider** — Unity UI uses \`data-theme="dark"\` for dark mode; load \`unity-ui-dark-mode\`
1534
+ 6. **Uninstall MUI** after all components are replaced
1535
+
1536
+ ## Component Map
1537
+
1538
+ | MUI | Unity UI | Skill |
1539
+ |-----|----------|-------|
1540
+ ${componentRows.join('\n')}
1541
+
1542
+ ## Unity UI–Only Components (no MUI equivalent)
1543
+
1544
+ ${noEquivalent}
1545
+
1546
+ ## Common Pattern Differences
1547
+
1548
+ | Pattern | MUI | Unity UI |
1549
+ |---------|-----|----------|
1550
+ | Theming | \`<ThemeProvider theme={theme}>\` | \`data-theme="dark"\` attribute |
1551
+ | Dark mode | \`createTheme({ palette: { mode: 'dark' } })\` | \`data-theme="dark"\` on container |
1552
+ | Inline styles | \`sx={{ mt: 2, color: 'primary.main' }}\` | Tailwind tokens: \`className="mt-md text-primary"\` |
1553
+ | Custom style prop | \`sx={{ ... }}\` | \`className\` with token classes |
1554
+ | Text field | \`<TextField variant="outlined" label="..." />\` | \`<TextField label="..." />\` |
1555
+ | Grid v2 | \`<Grid container spacing={2}>\` | \`<Grid columns={3} gap="md">\` |
1556
+ | Stack spacing | \`<Stack spacing={2}>\` | \`<Stack gap="md">\` |
1557
+ | Icons | \`import EditIcon from '@mui/icons-material/Edit'\` | \`<Icon name="Pencil" />\` |
1558
+ | Button loading | \`<LoadingButton loading>\` | \`<Button loading>\` |
1559
+ | Alert | \`<Alert severity="error">\` | \`<InfoBox variant="error">\` |
1560
+ | Snackbar | \`<Snackbar>...</Snackbar>\` (imperative) | \`addSnackbar()\` from \`useSnackbar()\` |
1561
+
1562
+ ## Replacing the sx Prop
1563
+
1564
+ MUI's \`sx\` prop maps CSS properties to theme values. Replace with Tailwind design tokens:
1565
+
1566
+ \`\`\`tsx
1567
+ // MUI
1568
+ <Box sx={{ mt: 2, mb: 3, color: 'text.secondary', bgcolor: 'background.paper' }}>
1569
+
1570
+ // Unity UI
1571
+ <Stack direction="column" gap="md" className="text-on-surface-variant bg-surface">
1572
+ \`\`\`
1573
+
1574
+ See the \`unity-ui-styling\` skill for the full token reference.
1575
+
1576
+ ## Removing MUI
1577
+
1578
+ \`\`\`bash
1579
+ npm uninstall @mui/material @mui/x-date-pickers @mui/icons-material @emotion/react @emotion/styled
1580
+ \`\`\`
1581
+
1582
+ ## Rules
1583
+
1584
+ - NEVER keep MUI and Unity UI components side-by-side — migrate fully
1585
+ - The \`sx\` prop does not exist in Unity UI — use \`className\` with token classes or component props
1586
+ - MUI icons do not exist in Unity UI — use \`<Icon name="..." />\` (load \`unity-ui-icons\` skill for names)
1587
+ - Remove \`ThemeProvider\` and \`CssBaseline\` from MUI; Unity UI uses CSS custom properties from \`theme.css\`
1588
+ - Import everything from \`@zvoove/unity-ui\`
1589
+ `
1590
+ );
1591
+ }
1592
+
1593
+ function generateFigmaCodeConnectSkill() {
1594
+ const description = [
1595
+ 'Figma Code Connect setup for @zvoove/unity-ui — links Figma components to Unity UI React components.',
1596
+ 'TRIGGER when: user wants to set up Figma Code Connect, link design components to code, or improve design-to-code accuracy in a Unity UI project.',
1597
+ 'DO NOT use this for general Figma-to-code generation — Code Connect is for mapping design components to existing code components.',
1598
+ ].join(String.raw`\n`);
1599
+
1600
+ const frontmatter = [
1601
+ '---',
1602
+ 'name: unity-ui-figma-code-connect',
1603
+ `description: "${description}"`,
1604
+ 'category: tooling',
1605
+ '---',
1606
+ ].join('\n');
1607
+
1608
+ return (
1609
+ frontmatter +
1610
+ `\n\n# Unity UI — Figma Code Connect
1611
+
1612
+ > Code Connect links Figma components to Unity UI React components so that when a designer shares a Figma URL, the AI knows exactly which component and props to use.
1613
+
1614
+ ## Install
1615
+
1616
+ \`\`\`bash
1617
+ npm install --save-dev @figma/code-connect
1618
+ \`\`\`
1619
+
1620
+ ## Configuration
1621
+
1622
+ Create \`figma.config.json\` at the project root:
1623
+
1624
+ \`\`\`json
1625
+ {
1626
+ "codeConnect": {
1627
+ "parser": "react",
1628
+ "include": ["src/**/*.figma.tsx"],
1629
+ "importPaths": {
1630
+ "@zvoove/unity-ui": "@zvoove/unity-ui"
1631
+ }
1632
+ }
1633
+ }
1634
+ \`\`\`
1635
+
1636
+ ## How to Get the Figma Node ID
1637
+
1638
+ 1. Open the Unity UI Figma design file
1639
+ 2. Select the component you want to connect
1640
+ 3. Copy the URL — it contains \`node-id=<nodeId>\`
1641
+ 4. Convert \`-\` to \`:\` in the node ID (e.g. \`123-456\` → \`123:456\`)
1642
+
1643
+ ## Mapping File Structure
1644
+
1645
+ Create a \`.figma.tsx\` file next to each component (or in a \`figma/\` folder):
1646
+
1647
+ \`\`\`tsx
1648
+ // src/figma/Button.figma.tsx
1649
+ import figma from '@figma/code-connect';
1650
+ import { Button } from '@zvoove/unity-ui';
1651
+
1652
+ figma.connect(
1653
+ Button,
1654
+ 'https://www.figma.com/design/<FILE_KEY>/<FILE_NAME>?node-id=<NODE_ID>',
1655
+ {
1656
+ props: {
1657
+ variant: figma.enum('Variant', {
1658
+ filled: 'filled',
1659
+ outlined: 'outlined',
1660
+ ghost: 'ghost',
1661
+ text: 'text',
1662
+ }),
1663
+ size: figma.enum('Size', {
1664
+ sm: 'sm',
1665
+ md: 'md',
1666
+ lg: 'lg',
1667
+ }),
1668
+ disabled: figma.boolean('Disabled'),
1669
+ label: figma.string('Label'),
1670
+ },
1671
+ example: ({ variant, size, disabled, label }) => (
1672
+ <Button variant={variant} size={size} disabled={disabled}>
1673
+ {label}
1674
+ </Button>
1675
+ ),
1676
+ }
1677
+ );
1678
+ \`\`\`
1679
+
1680
+ ## Example: TextField
1681
+
1682
+ \`\`\`tsx
1683
+ // src/figma/TextField.figma.tsx
1684
+ import figma from '@figma/code-connect';
1685
+ import { TextField } from '@zvoove/unity-ui';
1686
+
1687
+ figma.connect(
1688
+ TextField,
1689
+ 'https://www.figma.com/design/<FILE_KEY>/<FILE_NAME>?node-id=<NODE_ID>',
1690
+ {
1691
+ props: {
1692
+ label: figma.string('Label'),
1693
+ placeholder: figma.string('Placeholder'),
1694
+ error: figma.boolean('Error'),
1695
+ disabled: figma.boolean('Disabled'),
1696
+ },
1697
+ example: ({ label, placeholder, error, disabled }) => (
1698
+ <TextField
1699
+ label={label}
1700
+ placeholder={placeholder}
1701
+ error={error}
1702
+ disabled={disabled}
1703
+ />
1704
+ ),
1705
+ }
1706
+ );
1707
+ \`\`\`
1708
+
1709
+ ## Publish to Figma
1710
+
1711
+ \`\`\`bash
1712
+ # Preview what will be published (dry run)
1713
+ npx figma connect publish --dry-run --token <FIGMA_ACCESS_TOKEN>
1714
+
1715
+ # Publish
1716
+ npx figma connect publish --token <FIGMA_ACCESS_TOKEN>
1717
+ \`\`\`
1718
+
1719
+ Store the token in \`.env.local\` (never commit it):
1720
+
1721
+ \`\`\`bash
1722
+ FIGMA_ACCESS_TOKEN=figd_xxxxxxxx
1723
+ \`\`\`
1724
+
1725
+ \`\`\`bash
1726
+ npx figma connect publish --token $FIGMA_ACCESS_TOKEN
1727
+ \`\`\`
1728
+
1729
+ ## Figma Prop Types Reference
1730
+
1731
+ | Figma layer type | Code Connect helper | Example |
1732
+ |-----------------|---------------------|---------|
1733
+ | Variant property | \`figma.enum()\` | \`figma.enum('Variant', { filled: 'filled' })\` |
1734
+ | Boolean property | \`figma.boolean()\` | \`figma.boolean('Disabled')\` |
1735
+ | Text layer | \`figma.string()\` | \`figma.string('Label')\` |
1736
+ | Instance swap | \`figma.instance()\` | \`figma.instance('Icon')\` |
1737
+ | Nested instance | \`figma.nestedProps()\` | \`figma.nestedProps('Slot', { ... })\` |
1738
+
1739
+ ## Rules
1740
+
1741
+ - Create one \`.figma.tsx\` file per component
1742
+ - The Figma URL must point to the **main component** (not an instance or frame)
1743
+ - Prop names in \`figma.enum()\` must match Figma's exact property names (case-sensitive)
1744
+ - Never commit Figma access tokens — use environment variables
1745
+ - After updating, republish with \`npx figma connect publish\`
1746
+ `
1747
+ );
1748
+ }
1749
+
1750
+ function generateUmbrellaSkill(componentNames) {
1751
+ const description = [
1752
+ 'Unity UI (@zvoove/unity-ui) component library router. Use this as the entry point when building UI with this package.',
1753
+ 'TRIGGER when: user wants to build UI, use a component, or migrate/convert a UI from any source (Lovable, Figma Make, shadcn/ui, MUI, Ant Design, etc.) into this design system.',
1754
+ 'DO NOT TRIGGER when: @zvoove/unity-ui is not installed in the project.',
1755
+ ].join(String.raw`\n`);
1756
+
1757
+ const componentList = componentNames
1758
+ .map((n) => `- ${n} → load skill \`unity-ui-${kebabCase(n)}\``)
1759
+ .join('\n');
1760
+
1761
+ const frontmatter = [
1762
+ '---',
1763
+ 'name: unity-ui',
1764
+ `description: "${description}"`,
1765
+ 'category: routing',
1766
+ '---',
1767
+ ].join('\n');
1768
+
1769
+ return (
1770
+ frontmatter +
1771
+ `\n\n# Unity UI — Component Router
1772
+
1773
+ > **@zvoove/unity-ui** is installed in this project. Use the skills below — do NOT read \`dist/\`, \`node_modules/\`, or source files for API documentation.
1774
+
1775
+ ## How to Use
1776
+
1777
+ 1. Find the component you need in the directory below
1778
+ 2. Load its \`unity-ui-<component>\` skill for full props, examples, and rules
1779
+ 3. Import exclusively from \`@zvoove/unity-ui\` — never from dist paths
1780
+
1781
+ ## Component Directory
1782
+
1783
+ ${componentList}
1784
+
1785
+ ## Rules
1786
+
1787
+ - NEVER read \`dist/\` or \`node_modules/@zvoove/unity-ui/\` for type/API information — skills have everything
1788
+ - NEVER create custom components for functionality already provided by this library
1789
+ - Always import from \`@zvoove/unity-ui\`
1790
+ - If unsure which component to use, load \`unity-ui-index\` for the full shadcn/ui equivalence table
1791
+
1792
+ ## Migrating from Other Libraries
1793
+
1794
+ If the project contains components from shadcn/ui, MUI, Ant Design, Lovable, Figma Make, or any other source:
1795
+ - **Replace them with Unity UI equivalents** — do not keep mixed component libraries
1796
+ - Load \`unity-ui-index\` to find the equivalent Unity UI component
1797
+ - Remove the replaced library's imports and dependencies once converted
1798
+ - Prefer Unity UI over any other component library, regardless of what already exists in the codebase
1799
+ `
1800
+ );
1801
+ }
1802
+
618
1803
  // ─── Write skills to disk ───────────────────────────────────────────────────
619
1804
 
620
1805
  function writeSkill(outputDir, slug, content) {
@@ -664,18 +1849,66 @@ function main() {
664
1849
  }
665
1850
 
666
1851
  // Meta skills
1852
+ writeSkill(outputDir, 'unity-ui', generateUmbrellaSkill(componentNames));
667
1853
  writeSkill(outputDir, 'unity-ui-setup', generateSetupSkill(parsed));
1854
+ writeSkill(outputDir, 'unity-ui-styling', generateStylingSkill(parsed));
1855
+ writeSkill(
1856
+ outputDir,
1857
+ 'unity-ui-custom-component',
1858
+ generateCustomComponentSkill(parsed)
1859
+ );
668
1860
  writeSkill(outputDir, 'unity-ui-icons', generateIconsSkill(parsed));
669
1861
  writeSkill(outputDir, 'unity-ui-index', generateIndexSkill(componentNames));
670
- count += 3;
1862
+ writeSkill(outputDir, 'unity-ui-forms', generateFormsSkill());
1863
+ writeSkill(outputDir, 'unity-ui-layouts', generateLayoutsSkill());
1864
+ writeSkill(outputDir, 'unity-ui-dark-mode', generateDarkModeSkill());
1865
+ writeSkill(
1866
+ outputDir,
1867
+ 'unity-ui-migrate-shadcn',
1868
+ generateMigrateShadcnSkill(componentNames)
1869
+ );
1870
+ writeSkill(
1871
+ outputDir,
1872
+ 'unity-ui-migrate-mui',
1873
+ generateMigrateMuiSkill(componentNames)
1874
+ );
1875
+ writeSkill(
1876
+ outputDir,
1877
+ 'unity-ui-figma-code-connect',
1878
+ generateFigmaCodeConnectSkill()
1879
+ );
1880
+ count += 12;
671
1881
 
672
1882
  const relOutput = path.relative(projectRoot, outputDir);
673
1883
  console.log(`\n Unity UI — Agent Skills Generator\n`);
674
1884
  console.log(` ✅ Generated ${count} skills in ${relOutput}/\n`);
675
1885
  console.log(` ${componentNames.length} component skills`);
1886
+ console.log(` 1 umbrella routing skill (entry point + migration rules)`);
676
1887
  console.log(` 1 setup skill (installation, spacing, responsive props)`);
1888
+ console.log(
1889
+ ` 1 styling skill (Tailwind v4 tokens, tv(), no inline styles)`
1890
+ );
1891
+ console.log(
1892
+ ` 1 custom component skill (patterns for creating new components)`
1893
+ );
677
1894
  console.log(` 1 icons skill (all icon names)`);
678
- console.log(` 1 index skill (shadcn/ui mapping table)\n`);
1895
+ console.log(` 1 index skill (shadcn/ui + MUI mapping tables)`);
1896
+ console.log(
1897
+ ` 1 forms skill (react-hook-form + zod + Controller pattern)`
1898
+ );
1899
+ console.log(
1900
+ ` 1 layouts skill (app shells, dashboards, settings, master/detail)`
1901
+ );
1902
+ console.log(
1903
+ ` 1 dark mode skill (data-theme, ThemeProvider, token behaviour)`
1904
+ );
1905
+ console.log(
1906
+ ` 1 migrate-shadcn skill (step-by-step shadcn/ui → Unity UI)`
1907
+ );
1908
+ console.log(` 1 migrate-mui skill (step-by-step MUI → Unity UI)`);
1909
+ console.log(
1910
+ ` 1 figma-code-connect skill (Code Connect setup and mappings)\n`
1911
+ );
679
1912
  console.log(` Your AI agent can now discover and use Unity UI components.`);
680
1913
  console.log(` Skills are loaded on-demand — only relevant ones are read.\n`);
681
1914
  }