@preply/ds-ai-core 11.1.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.
Files changed (104) hide show
  1. package/AGENTS.md +7 -0
  2. package/README.md +34 -0
  3. package/dist/event-tracking.d.ts +21 -0
  4. package/dist/event-tracking.d.ts.map +1 -0
  5. package/dist/index.d.ts +5 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +38209 -0
  8. package/dist/tools/components/__tests__/get-component-docs.test.d.ts +2 -0
  9. package/dist/tools/components/__tests__/get-component-docs.test.d.ts.map +1 -0
  10. package/dist/tools/components/__tests__/list-components.test.d.ts +2 -0
  11. package/dist/tools/components/__tests__/list-components.test.d.ts.map +1 -0
  12. package/dist/tools/components/context/__test__/get-components-data.test.d.ts +2 -0
  13. package/dist/tools/components/context/__test__/get-components-data.test.d.ts.map +1 -0
  14. package/dist/tools/components/context/__test__/test-components/FixtureButton.d.ts +15 -0
  15. package/dist/tools/components/context/__test__/test-components/FixtureButton.d.ts.map +1 -0
  16. package/dist/tools/components/context/__test__/test-components/index.d.ts +2 -0
  17. package/dist/tools/components/context/__test__/test-components/index.d.ts.map +1 -0
  18. package/dist/tools/components/context/extract-component-docgen-info.d.ts +3 -0
  19. package/dist/tools/components/context/extract-component-docgen-info.d.ts.map +1 -0
  20. package/dist/tools/components/context/get-components-data.d.ts +24 -0
  21. package/dist/tools/components/context/get-components-data.d.ts.map +1 -0
  22. package/dist/tools/components/context/index.d.ts +6 -0
  23. package/dist/tools/components/context/index.d.ts.map +1 -0
  24. package/dist/tools/components/context/render-component-doc.d.ts +4 -0
  25. package/dist/tools/components/context/render-component-doc.d.ts.map +1 -0
  26. package/dist/tools/components/get-component-docs.d.ts +15 -0
  27. package/dist/tools/components/get-component-docs.d.ts.map +1 -0
  28. package/dist/tools/components/list-components.d.ts +9 -0
  29. package/dist/tools/components/list-components.d.ts.map +1 -0
  30. package/dist/tools/index.d.ts +6 -0
  31. package/dist/tools/index.d.ts.map +1 -0
  32. package/dist/tools/search-icon/__tests__/get-icons-data.test.d.ts +2 -0
  33. package/dist/tools/search-icon/__tests__/get-icons-data.test.d.ts.map +1 -0
  34. package/dist/tools/search-icon/__tests__/search-icon.test.d.ts +2 -0
  35. package/dist/tools/search-icon/__tests__/search-icon.test.d.ts.map +1 -0
  36. package/dist/tools/search-icon/get-icons-data.d.ts +6 -0
  37. package/dist/tools/search-icon/get-icons-data.d.ts.map +1 -0
  38. package/dist/tools/search-icon/search-icon.d.ts +18 -0
  39. package/dist/tools/search-icon/search-icon.d.ts.map +1 -0
  40. package/dist/tools/search-token/__tests__/search-token-by-name.test.d.ts +2 -0
  41. package/dist/tools/search-token/__tests__/search-token-by-name.test.d.ts.map +1 -0
  42. package/dist/tools/search-token/__tests__/search-token-by-value.test.d.ts +2 -0
  43. package/dist/tools/search-token/__tests__/search-token-by-value.test.d.ts.map +1 -0
  44. package/dist/tools/search-token/context/__tests__/get-tokens-data.test.d.ts +2 -0
  45. package/dist/tools/search-token/context/__tests__/get-tokens-data.test.d.ts.map +1 -0
  46. package/dist/tools/search-token/context/get-tokens-data.d.ts +13 -0
  47. package/dist/tools/search-token/context/get-tokens-data.d.ts.map +1 -0
  48. package/dist/tools/search-token/context/index.d.ts +6 -0
  49. package/dist/tools/search-token/context/index.d.ts.map +1 -0
  50. package/dist/tools/search-token/search-token-by-name.d.ts +26 -0
  51. package/dist/tools/search-token/search-token-by-name.d.ts.map +1 -0
  52. package/dist/tools/search-token/search-token-by-value.d.ts +24 -0
  53. package/dist/tools/search-token/search-token-by-value.d.ts.map +1 -0
  54. package/dist/tools/search-token/token-utils.d.ts +9 -0
  55. package/dist/tools/search-token/token-utils.d.ts.map +1 -0
  56. package/dist/types.d.ts +3 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/utils/create-search-index.d.ts +48 -0
  59. package/dist/utils/create-search-index.d.ts.map +1 -0
  60. package/dist/utils/define-tool.d.ts +49 -0
  61. package/dist/utils/define-tool.d.ts.map +1 -0
  62. package/dist/utils/format-list.d.ts +26 -0
  63. package/dist/utils/format-list.d.ts.map +1 -0
  64. package/dist/utils/md.d.ts +24 -0
  65. package/dist/utils/md.d.ts.map +1 -0
  66. package/package.json +48 -0
  67. package/src/event-tracking.ts +117 -0
  68. package/src/index.ts +4 -0
  69. package/src/tools/components/__tests__/get-component-docs.test.ts +58 -0
  70. package/src/tools/components/__tests__/list-components.test.ts +63 -0
  71. package/src/tools/components/context/__test__/get-components-data.test.ts +57 -0
  72. package/src/tools/components/context/__test__/test-components/FixtureButton.tsx +18 -0
  73. package/src/tools/components/context/__test__/test-components/index.ts +1 -0
  74. package/src/tools/components/context/__test__/test-components/tsconfig.json +11 -0
  75. package/src/tools/components/context/extract-component-docgen-info.ts +108 -0
  76. package/src/tools/components/context/get-components-data.ts +94 -0
  77. package/src/tools/components/context/index.ts +4 -0
  78. package/src/tools/components/context/render-component-doc.ts +89 -0
  79. package/src/tools/components/get-component-docs.ts +26 -0
  80. package/src/tools/components/list-components.ts +36 -0
  81. package/src/tools/index.ts +5 -0
  82. package/src/tools/search-icon/__tests__/get-icons-data.test.ts +22 -0
  83. package/src/tools/search-icon/__tests__/search-icon.test.ts +235 -0
  84. package/src/tools/search-icon/__tests__/test-icons/NotIcon.md +1 -0
  85. package/src/tools/search-icon/__tests__/test-icons/OtherIcon.svg +1 -0
  86. package/src/tools/search-icon/__tests__/test-icons/TokyoUIClose.svg +1 -0
  87. package/src/tools/search-icon/__tests__/test-icons/TokyoUIHelp.svg +1 -0
  88. package/src/tools/search-icon/get-icons-data.ts +19 -0
  89. package/src/tools/search-icon/search-icon.ts +100 -0
  90. package/src/tools/search-token/__tests__/search-token-by-name.test.ts +384 -0
  91. package/src/tools/search-token/__tests__/search-token-by-value.test.ts +250 -0
  92. package/src/tools/search-token/context/__tests__/get-tokens-data.test.ts +148 -0
  93. package/src/tools/search-token/context/get-tokens-data.ts +103 -0
  94. package/src/tools/search-token/context/index.ts +4 -0
  95. package/src/tools/search-token/search-token-by-name.ts +110 -0
  96. package/src/tools/search-token/search-token-by-value.ts +107 -0
  97. package/src/tools/search-token/token-utils.ts +60 -0
  98. package/src/types.ts +3 -0
  99. package/src/utils/create-search-index.ts +121 -0
  100. package/src/utils/define-tool.ts +67 -0
  101. package/src/utils/format-list.ts +38 -0
  102. package/src/utils/md.ts +12 -0
  103. package/tsconfig.json +11 -0
  104. package/vite.config.ts +23 -0
@@ -0,0 +1,148 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ tokens: {
5
+ spacing: {
6
+ '4': ['base', 'abc123', false],
7
+ '8': ['base', 'def456', false],
8
+ },
9
+ root: {
10
+ radius: {
11
+ xs: ['base', 'ghi789', false],
12
+ },
13
+ },
14
+ color: {
15
+ text: {
16
+ primary: ['base', 'aaa111', true],
17
+ },
18
+ },
19
+ } as unknown as typeof import('@preply/ds-core').tokens,
20
+ valuesWeb: {
21
+ tabc123: '4px',
22
+ tdef456: '8px',
23
+ tghi789: 2,
24
+ taaa111: { base: '#111111' },
25
+ } as unknown as typeof import('@preply/ds-theme-tokyo-ui').VALUES_WEB,
26
+ valuesRn: {
27
+ tabc123: 4,
28
+ tdef456: 8,
29
+ tghi789: 2,
30
+ taaa111: { base: '#101010' },
31
+ } as unknown as typeof import('@preply/ds-theme-tokyo-ui').VALUES_RN,
32
+ }));
33
+
34
+ vi.mock(import('@preply/ds-theme-tokyo-ui'), async () => ({
35
+ VALUES_WEB: mocks.valuesWeb,
36
+ VALUES_RN: mocks.valuesRn,
37
+ }));
38
+
39
+ vi.mock(import('@preply/ds-core'), async () => ({
40
+ ...(await import('@preply/ds-core')),
41
+ tokens: mocks.tokens,
42
+ }));
43
+
44
+ beforeEach(() => {
45
+ vi.resetModules();
46
+ vi.resetAllMocks();
47
+ });
48
+
49
+ describe('getTokensData', () => {
50
+ it('flattens token tree and computes formatted names for web', async () => {
51
+ const { getTokensData } = await import('../get-tokens-data');
52
+ const data = getTokensData();
53
+
54
+ expect(data.web).toMatchInlineSnapshot(`
55
+ [
56
+ {
57
+ "less": "@spacing-4",
58
+ "name": "spacing.4",
59
+ "private": false,
60
+ "scss": "$spacing-4",
61
+ "typescript": "spacing[4]",
62
+ "value": "4px",
63
+ },
64
+ {
65
+ "less": "@spacing-8",
66
+ "name": "spacing.8",
67
+ "private": false,
68
+ "scss": "$spacing-8",
69
+ "typescript": "spacing[8]",
70
+ "value": "8px",
71
+ },
72
+ {
73
+ "less": "@root-radius-xs",
74
+ "name": "root.radius.xs",
75
+ "private": true,
76
+ "scss": "$root-radius-xs",
77
+ "typescript": "root.radius.xs",
78
+ "value": 2,
79
+ },
80
+ {
81
+ "less": "@color-text-primary",
82
+ "name": "color.text.primary",
83
+ "private": false,
84
+ "scss": "$color-text-primary",
85
+ "typescript": "color.text.primary",
86
+ "value": "#111111",
87
+ },
88
+ ]
89
+ `);
90
+ });
91
+
92
+ it('resolves react-native values using VALUES_RN map', async () => {
93
+ const { getTokensData } = await import('../get-tokens-data');
94
+ const data = getTokensData();
95
+
96
+ expect(data['react-native']).toMatchInlineSnapshot(`
97
+ [
98
+ {
99
+ "less": "@spacing-4",
100
+ "name": "spacing.4",
101
+ "private": false,
102
+ "scss": "$spacing-4",
103
+ "typescript": "spacing[4]",
104
+ "value": 4,
105
+ },
106
+ {
107
+ "less": "@spacing-8",
108
+ "name": "spacing.8",
109
+ "private": false,
110
+ "scss": "$spacing-8",
111
+ "typescript": "spacing[8]",
112
+ "value": 8,
113
+ },
114
+ {
115
+ "less": "@root-radius-xs",
116
+ "name": "root.radius.xs",
117
+ "private": true,
118
+ "scss": "$root-radius-xs",
119
+ "typescript": "root.radius.xs",
120
+ "value": 2,
121
+ },
122
+ {
123
+ "less": "@color-text-primary",
124
+ "name": "color.text.primary",
125
+ "private": false,
126
+ "scss": "$color-text-primary",
127
+ "typescript": "color.text.primary",
128
+ "value": "#101010",
129
+ },
130
+ ]
131
+ `);
132
+ });
133
+
134
+ it('throws when token value cannot be resolved', async () => {
135
+ vi.doMock('@preply/ds-core', () => ({
136
+ isToken: (value: unknown) => Array.isArray(value),
137
+ tokens: {
138
+ spacing: {
139
+ '16': ['base', 'missing', false],
140
+ },
141
+ },
142
+ }));
143
+
144
+ const { getTokensData } = await import('../get-tokens-data');
145
+
146
+ expect(() => getTokensData()).toThrow('Token value not found for token id: missing');
147
+ });
148
+ });
@@ -0,0 +1,103 @@
1
+ import { isToken, tokens } from '@preply/ds-core';
2
+ import { VALUES_RN, VALUES_WEB } from '@preply/ds-theme-tokyo-ui';
3
+ import { ThemeTokenValue, Token, TOKEN_ID_FIELD } from '@preply/ds-core-types';
4
+ import { Platform } from '../../../types';
5
+
6
+ const PRIVATE_TOKEN_PATTERNS = ['dsInternalPrimitive', 'root'];
7
+
8
+ /** @example formatScssName('dropShadow.1.shorthand') // '$drop-shadow-1-shorthand' */
9
+ const formatScssName = (name: string) => '$' + name.replace(/\./g, '-');
10
+ /** @example formatLessName('dropShadow.1.shorthand') // '@drop-shadow-1-shorthand' */
11
+ const formatLessName = (name: string) => '@' + name.replace(/\./g, '-');
12
+ /** @example formatTypescriptName('dropShadow.1.shorthand') // 'dropShadow[1].shorthand' */
13
+ const formatTypescriptName = (name: string) =>
14
+ name
15
+ .split('.')
16
+ .map(part => {
17
+ const isNumeric = !isNaN(Number(part));
18
+ const hasHyphen = part.includes('-');
19
+ if (isNumeric || hasHyphen) return `[${part}]`;
20
+ return part;
21
+ })
22
+ .join('.')
23
+ .replace(/\.\[/g, '[');
24
+
25
+ /**
26
+ * Resolves a token's numeric ID to its concrete theme value.
27
+ * Theme values are keyed as `t{id}` and can be:
28
+ * - a primitive (string | number) -> returned directly
29
+ * - an object with a `.base` key -> `.base` is the canonical value (responsive tokens)
30
+ * - an object without `.base` -> first value is used as fallback (e.g. platform-specific)
31
+ */
32
+ function getTokenValue(token: Token, values: Record<string, ThemeTokenValue>): string | number {
33
+ const tokenId = token[TOKEN_ID_FIELD];
34
+ const value = values[`t${tokenId}`];
35
+ if (value === undefined) {
36
+ throw new Error(`Token value not found for token id: ${tokenId}`);
37
+ }
38
+
39
+ if (typeof value === 'object') {
40
+ const base = (value as Record<string, string | number>).base;
41
+ if (base !== undefined) return base;
42
+
43
+ const firstValue = Object.values(value)[0];
44
+ if (firstValue !== undefined) return firstValue as string | number;
45
+ }
46
+
47
+ return value as string | number;
48
+ }
49
+
50
+ /** Recursively walks the nested token tree, collecting leaf tokens (identified by `isToken`) into a flat array. */
51
+ function flattenTokens(
52
+ input: unknown,
53
+ values: Record<string, ThemeTokenValue>,
54
+ path: string[] = [],
55
+ output: TokenData[] = [],
56
+ ): TokenData[] {
57
+ if (isToken(input)) {
58
+ const name = path.join('.');
59
+ const value = getTokenValue(input, values);
60
+ output.push({
61
+ name,
62
+ scss: formatScssName(name),
63
+ less: formatLessName(name),
64
+ typescript: formatTypescriptName(name),
65
+ value,
66
+ private: PRIVATE_TOKEN_PATTERNS.some(pattern => name.startsWith(pattern)),
67
+ });
68
+ return output;
69
+ }
70
+
71
+ if (!input || typeof input !== 'object') {
72
+ return output;
73
+ }
74
+
75
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
76
+ flattenTokens(value, values, [...path, key], output);
77
+ }
78
+
79
+ return output;
80
+ }
81
+
82
+ export type TokenData = {
83
+ name: string;
84
+ scss: string;
85
+ less: string;
86
+ typescript: string;
87
+ value: string | number;
88
+ private: boolean;
89
+ };
90
+
91
+ type PlatformTokens = Record<Platform, TokenData[]>;
92
+
93
+ export function getTokensData(): PlatformTokens {
94
+ const valuesByPlatform: Record<Platform, Record<string, ThemeTokenValue>> = {
95
+ web: VALUES_WEB as Record<string, ThemeTokenValue>,
96
+ 'react-native': VALUES_RN as Record<string, ThemeTokenValue>,
97
+ };
98
+
99
+ return {
100
+ web: flattenTokens(tokens, valuesByPlatform.web),
101
+ 'react-native': flattenTokens(tokens, valuesByPlatform['react-native']),
102
+ };
103
+ }
@@ -0,0 +1,4 @@
1
+ // Build-time macro: getTokensData() runs at compile time and its return value is inlined into the bundle
2
+ import { getTokensData } from './get-tokens-data' with { type: 'macro' };
3
+
4
+ export default getTokensData();
@@ -0,0 +1,110 @@
1
+ import { createSearchIndex } from '../../utils/create-search-index';
2
+ import { PLATFORMS } from '../../types';
3
+ import dedent from 'dedent';
4
+ import { defineTool } from '../../utils/define-tool';
5
+ import * as z from 'zod';
6
+ import { stringify } from 'yaml';
7
+ import { formatList } from '../../utils/format-list';
8
+ import tokens from './context';
9
+ import { Format, formatToken, getExample } from './token-utils';
10
+
11
+ export type { Format };
12
+
13
+ /**
14
+ * Normalizes token name to the unified format, so queries in all formats will match the same token:
15
+ * - dropShadow.1.shorthand
16
+ * - $dropShadow-1-shorthand
17
+ * - @dropShadow-1-shorthand
18
+ * - dropShadow[1].shorthand
19
+ */
20
+ const normalize = (term: string) =>
21
+ term
22
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
23
+ .trim()
24
+ .toLowerCase();
25
+
26
+ const indexes = {
27
+ web: createSearchIndex(tokens.web, { ref: 'name', normalize }),
28
+ 'react-native': createSearchIndex(tokens['react-native'], { ref: 'name', normalize }),
29
+ };
30
+
31
+ export const searchTokenByName = defineTool({
32
+ name: 'search_token_by_name',
33
+ description: 'Search for a design system style tokens by name',
34
+ arguments: z.object({
35
+ name: z.string().describe(
36
+ dedent`
37
+ The token name to search for. This can be in any format:
38
+ - raw token name (e.g. dropShadow.1.shorthand)
39
+ - scss (e.g. $dropShadow-1-shorthand)
40
+ - less (e.g. @dropShadow-1-shorthand)
41
+ - typescript (e.g. dropShadow[1].shorthand)
42
+ `,
43
+ ),
44
+ }),
45
+ options: z.object({
46
+ platform: z.enum(PLATFORMS).describe('Target platform for token values. Required.'),
47
+ format: z.enum(['scss', 'less', 'typescript']).optional().default('scss').describe(dedent`
48
+ Optional output format for web platform only. Defaults to "scss".
49
+ - "scss" - SCSS variable (e.g. $drop-shadow-1-shorthand)
50
+ - "less" - Less variable (e.g. @drop-shadow-1-shorthand)
51
+ - "typescript" - Typescript variable for usage in styled-components (e.g. dropShadow[1].shorthand)
52
+
53
+ For "react-native" platform this option is ignored.
54
+ `),
55
+ includeExample: z
56
+ .boolean()
57
+ .default(true)
58
+ .describe('Whether to include an usage example. Default: true'),
59
+ includePrivate: z.boolean().default(false).describe(dedent`
60
+ Whether to include private tokens. Default: false
61
+
62
+ Private tokens starts from "dsInternalPrimitive" or "root" prefixes.
63
+ They're not intended for public use, however they're still available in the system.
64
+ Only use this option if you wasn't able to find public token or you're confident that private token is what you need.
65
+ `),
66
+ }),
67
+ callback: ({ name, platform, format = 'scss', includeExample, includePrivate }) => {
68
+ const outputFormat: Format = platform === 'web' ? format : 'scss';
69
+ const matches = indexes[platform]
70
+ .search(name, 10)
71
+ .filter(m => includePrivate || !m.item.private);
72
+
73
+ if (!matches.length) {
74
+ return [`No results found for "${name}"`];
75
+ }
76
+
77
+ const [first, ...rest] = matches;
78
+
79
+ const examples = includeExample
80
+ ? [
81
+ platform === 'web'
82
+ ? `Here is an example of how to use design-system tokens in ${outputFormat}:`
83
+ : 'Here is an example of how to use design-system tokens in react-native:',
84
+ getExample(platform, outputFormat),
85
+ ]
86
+ : [];
87
+
88
+ if (!first.exact) {
89
+ return [
90
+ `There is no exact match for "${name}", but here are some similar tokens:`,
91
+ formatList(matches.map(m => formatToken(m.item, platform, outputFormat))),
92
+ ...examples,
93
+ ];
94
+ }
95
+
96
+ const similarMessages = rest.length
97
+ ? [
98
+ 'Also, here you can find some tokens with similar names:',
99
+ formatList(rest.map(m => formatToken(m.item, platform, outputFormat))),
100
+ ]
101
+ : [];
102
+
103
+ return [
104
+ `Found exact match for "${name}":`,
105
+ stringify(formatToken(first.item, platform, outputFormat)).trim(),
106
+ ...examples,
107
+ ...similarMessages,
108
+ ];
109
+ },
110
+ });
@@ -0,0 +1,107 @@
1
+ // Build-time macro: getTokensData() runs at compile time and its return value is inlined into the bundle
2
+ import { TokenData, getTokensData } from './context/get-tokens-data' with { type: 'macro' };
3
+ import { PLATFORMS, Platform } from '../../types';
4
+ import dedent from 'dedent';
5
+ import { defineTool } from '../../utils/define-tool';
6
+ import * as z from 'zod';
7
+ import { formatList } from '../../utils/format-list';
8
+ import { Format, formatToken, getExample } from './token-utils';
9
+
10
+ const allTokens = getTokensData();
11
+
12
+ function buildValueMap(tokens: TokenData[]): Record<string, TokenData[]> {
13
+ const map: Record<string, TokenData[]> = {};
14
+ for (const token of tokens) {
15
+ const key = token.value.toString().toLowerCase();
16
+ map[key] = map[key] ?? [];
17
+ map[key].push(token);
18
+ }
19
+ return map;
20
+ }
21
+
22
+ const valueMaps = {
23
+ web: buildValueMap(allTokens.web),
24
+ 'react-native': buildValueMap(allTokens['react-native']),
25
+ };
26
+
27
+ function searchByValue(platform: Platform, value: string | number, includePrivate = false) {
28
+ const map = valueMaps[platform];
29
+ const normalizedValue = value.toString().toLowerCase();
30
+ let results = map[normalizedValue] ?? [];
31
+
32
+ if (!isNaN(Number(value))) {
33
+ const pxResults = map[`${normalizedValue}px`] ?? [];
34
+ const emResults = map[`${normalizedValue}em`] ?? [];
35
+ results = [...results, ...pxResults, ...emResults];
36
+ }
37
+
38
+ if (!includePrivate) {
39
+ results = results.filter(t => !t.private);
40
+ }
41
+
42
+ return [...new Set(results)];
43
+ }
44
+
45
+ export const searchTokenByValue = defineTool({
46
+ name: 'search_token_by_value',
47
+ description: 'Search for a design system style tokens by its value',
48
+ arguments: z.object({
49
+ value: z.string().describe(
50
+ dedent`
51
+ The token value to search for. This can be:
52
+ - numeric values (e.g. 500)
53
+ - colors (e.g. #000000 or rgba(0, 0, 0, 0.5))
54
+ - sizes (e.g. 16px or 2em)
55
+ - fonts (e.g. Inter or Arial)
56
+ - etc.
57
+ `,
58
+ ),
59
+ }),
60
+ options: z.object({
61
+ platform: z.enum(PLATFORMS).describe('Target platform for token values. Required.'),
62
+ format: z.enum(['scss', 'less', 'typescript']).optional().default('scss').describe(dedent`
63
+ Optional output format for web platform only. Defaults to "scss".
64
+ - "scss" - SCSS variable (e.g. $drop-shadow-1-shorthand)
65
+ - "less" - Less variable (e.g. @drop-shadow-1-shorthand)
66
+ - "typescript" - Typescript variable for usage in styled-components (e.g. dropShadow[1].shorthand)
67
+
68
+ For "react-native" platform this option is ignored.
69
+ `),
70
+ includeExample: z
71
+ .boolean()
72
+ .default(true)
73
+ .describe('Whether to include an usage example. Default: true'),
74
+ includePrivate: z.boolean().default(false).describe(dedent`
75
+ Whether to include private tokens. Default: false
76
+
77
+ Private tokens starts from "dsInternalPrimitive" or "root" prefixes.
78
+ They're not intended for public use, however they're still available in the system.
79
+ Only use this option if you wasn't able to find public token or you're confident that private token is what you need.
80
+ `),
81
+ }),
82
+ callback: ({ value, platform, format = 'scss', includeExample, includePrivate }) => {
83
+ const outputFormat: Format = platform === 'web' ? format : 'scss';
84
+ const results = searchByValue(platform, value, includePrivate);
85
+
86
+ if (!results.length) {
87
+ return [`No results found for "${value}"`];
88
+ }
89
+
90
+ const formattedResults = results.map(r => formatToken(r, platform, outputFormat));
91
+
92
+ const examples = includeExample
93
+ ? [
94
+ platform === 'web'
95
+ ? `Here is an example of how to use design-system tokens in ${outputFormat}:`
96
+ : 'Here is an example of how to use design-system tokens in react-native:',
97
+ getExample(platform, outputFormat),
98
+ ]
99
+ : [];
100
+
101
+ return [
102
+ `Found ${results.length} tokens with value "${value}"`,
103
+ formatList(formattedResults),
104
+ ...examples,
105
+ ];
106
+ },
107
+ });
@@ -0,0 +1,60 @@
1
+ import type { TokenData } from './context/get-tokens-data';
2
+ import { Platform } from '../../types';
3
+ import dedent from 'dedent';
4
+
5
+ export type Format = 'scss' | 'less' | 'typescript';
6
+
7
+ export function formatToken(token: TokenData, platform: Platform, format: Format) {
8
+ return {
9
+ token: platform === 'web' ? token[format] : token.typescript,
10
+ value: token.value,
11
+ };
12
+ }
13
+
14
+ const webExamples: Record<Format, string> = {
15
+ scss: dedent`
16
+ @use '@preply/ds-web-core/dist/generated/tokens' as *;
17
+
18
+ .MyComponent {
19
+ box-shadow: $dropShadow-1-shorthand;
20
+ }
21
+ `,
22
+ less: dedent`
23
+ @import '~@preply/ds-web-core/dist/generated/tokens.less';
24
+
25
+ .MyComponent {
26
+ box-shadow: @dropShadow-1-shorthand;
27
+ }
28
+ `,
29
+ typescript: dedent`
30
+ import styled from 'styled-components';
31
+ import { dropShadow } from '@preply/ds-core';
32
+ import { getTokenVar } from '@preply/ds-web-core';
33
+
34
+ const MyComponent = styled.div\`
35
+ box-shadow: \${getTokenVar(dropShadow[1].shorthand)};
36
+ \`;
37
+ `,
38
+ };
39
+
40
+ const reactNativeExample = dedent`
41
+ import { View } from 'react-native';
42
+ import { color } from '@preply/ds-core';
43
+ import { createStyleSheet, useStyleSheet } from '@preply/ds-rn-core';
44
+
45
+ const rawStyles = createStyleSheet({
46
+ container: {
47
+ backgroundColor: color.background.primary,
48
+ },
49
+ });
50
+
51
+ export const MyComponent = () => {
52
+ const styles = useStyleSheet(rawStyles);
53
+
54
+ return <View style={styles.container} />;
55
+ };
56
+ `;
57
+
58
+ export function getExample(platform: Platform, format: Format) {
59
+ return platform === 'web' ? webExamples[format] : reactNativeExample;
60
+ }
package/src/types.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type Platform = 'web' | 'react-native';
2
+
3
+ export const PLATFORMS = ['web', 'react-native'] as const satisfies Platform[];
@@ -0,0 +1,121 @@
1
+ /* eslint-disable import/no-named-as-default-member */
2
+ import lunr from 'lunr';
3
+
4
+ // Lunr requires globally unique names for registered pipeline functions.
5
+ // This counter ensures each createSearchIndex call gets a distinct function name.
6
+ let pipelineCounter = 0;
7
+
8
+ type Result<T> = {
9
+ item: T;
10
+ /** Whether the query matched a ref exactly (as opposed to a fuzzy/partial match). */
11
+ exact: boolean;
12
+ };
13
+
14
+ /**
15
+ * Extracts only the keys of T whose values are strings.
16
+ * Used to constrain `ref` so it always points to a string field.
17
+ */
18
+ type StringKeys<T extends Record<string, unknown>, K = keyof T> = K extends string
19
+ ? T[K] extends string
20
+ ? K
21
+ : never
22
+ : never;
23
+
24
+ /**
25
+ * Builds a lunr full-text search index over `items` and returns a thin wrapper
26
+ * with `search` (ranked results) and `get` (exact lookup) methods.
27
+ *
28
+ * @param items - The dataset to index.
29
+ * @param ref - Which string field to use as the unique document key.
30
+ * @param normalize - Optional transform applied to both stored refs and incoming
31
+ * queries so that e.g. case or naming-convention differences
32
+ * don't prevent matches. When provided, a custom pipeline
33
+ * function is also registered to split compound tokens.
34
+ */
35
+ export function createSearchIndex<T extends Record<string, unknown>>(
36
+ /** The dataset to index. */
37
+ items: T[],
38
+ options: {
39
+ /** The field to use as the unique document key. */
40
+ ref: StringKeys<T>;
41
+ /** An optional transform applied to both stored refs and incoming queries */
42
+ normalize?: (term: string) => string;
43
+ },
44
+ ) {
45
+ const { ref, normalize = s => s } = options;
46
+
47
+ const itemsByRef = new Map<string, T>();
48
+ for (const item of items) {
49
+ itemsByRef.set(normalize(item[ref] as string), item);
50
+ }
51
+
52
+ // When a normalize function is provided we also register a custom lunr
53
+ // pipeline step that splits compound tokens before stemming.
54
+ let pipelineFn: lunr.PipelineFunction | undefined;
55
+ if (options.normalize) {
56
+ const fnName = `createSearchIndex_normalize_${pipelineCounter++}`;
57
+ pipelineFn = (token: lunr.Token) => {
58
+ const tokenParts = token
59
+ .toString()
60
+ .replace(/[^a-zA-Z0-9]/g, ' ')
61
+ .trim()
62
+ .split(/\s+/);
63
+
64
+ return tokenParts.map(part => token.clone().update(() => part));
65
+ };
66
+ // Pipeline functions must be registered globally before they can be
67
+ // added to an index pipeline; the unique fnName avoids collisions.
68
+ lunr.Pipeline.registerFunction(pipelineFn, fnName);
69
+ }
70
+
71
+ // Build the lunr index. `__ref` is an internal field that holds the
72
+ // normalized ref — we use a synthetic name to avoid clashing with any
73
+ // field already present on the items.
74
+ const index = lunr(function () {
75
+ this.ref('__ref');
76
+ this.field('__ref');
77
+
78
+ if (pipelineFn) {
79
+ this.pipeline.before(lunr.stemmer, pipelineFn);
80
+ this.searchPipeline.before(lunr.stemmer, pipelineFn);
81
+ }
82
+
83
+ for (const item of items) {
84
+ this.add({
85
+ ...item,
86
+ __ref: normalize(item[ref] as string),
87
+ });
88
+ }
89
+ });
90
+
91
+ return {
92
+ /**
93
+ * Performs a full-text search over the indexed items.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const results = index.search('dropShadow-1-shorthand');
98
+ * console.log(results);
99
+ * // [
100
+ * // { item: { name: 'dropShadow-1-shorthand' }, exact: true },
101
+ * // { item: { name: 'dropShadow-1-shorthand' }, exact: false },
102
+ * // ]
103
+ * ```
104
+ */
105
+ search(query: string, limit?: number): Result<T>[] {
106
+ const results = index.search(normalize(query)).map(result => {
107
+ const item = itemsByRef.get(result.ref)!;
108
+ return {
109
+ item,
110
+ exact: result.ref === normalize(query),
111
+ };
112
+ });
113
+
114
+ return limit ? results.slice(0, limit) : results;
115
+ },
116
+ /** Direct lookup by normalized ref value. Returns undefined if not found. */
117
+ get(query: string): T | undefined {
118
+ return itemsByRef.get(normalize(query));
119
+ },
120
+ };
121
+ }