Skip to content

Commit 54c8073

Browse files
committed
feat(useTokens): add token options for depth and prefix
1 parent a87c3d3 commit 54c8073

File tree

3 files changed

+65
-10
lines changed

3 files changed

+65
-10
lines changed

apps/docs/src/pages/composables/registration/use-tokens.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The `useTokens` composable allows you to define a collection of design tokens, w
1111
```ts
1212
import { useTokens } from '@vuetify/v0'
1313

14+
// Default behavior (depth = Infinity): fully flatten nested objects
1415
const tokens = useTokens({
1516
color: {
1617
primary: '#3b82f6',
@@ -26,6 +27,14 @@ const tokens = useTokens({
2627
tokens.resolve('color.primary') // '#3b82f6'
2728
tokens.resolve('color.info') // '#3b82f6' (alias resolved)
2829
tokens.resolve('radius.md') // '8px'
30+
31+
const features = useTokens({
32+
dark: true,
33+
rtl: { value: true, variation: 'toggle' },
34+
}, { depth: 0 })
35+
36+
// With depth: 0, nested objects are kept as-is at their base id
37+
features.resolve('rtl') // { value: true, variation: 'toggle' }
2938
```
3039

3140
## API
@@ -57,10 +66,18 @@ tokens.resolve('radius.md') // '8px'
5766
resolve: (token: string | TokenAlias) => unknown | undefined
5867
}
5968

69+
interface TokenOptions {
70+
depth?: number
71+
prefix?: string
72+
}
73+
6074
function useTokens<
6175
Z extends TokenTicket = TokenTicket,
6276
E extends TokenContext<Z> = TokenContext<Z>,
63-
> (tokens: TokenCollection): TokenContext<Z>
77+
> (
78+
tokens: TokenCollection,
79+
options?: TokenOptions,
80+
): TokenContext<Z>
6481
```
6582

6683
* **Details**
@@ -69,13 +86,17 @@ tokens.resolve('radius.md') // '8px'
6986
* **Description**
7087
* Creates a token registry context initialized with an optional nested collection of tokens.
7188
* Flattens nested token collections into a flat list of token entries for registration.
89+
* The `depth` option lets you control how deep flattening goes (default: Infinity). Set `depth: 0` to keep nested objects intact at their base id.
7290
* Supports tokens defined as primitive values or as aliases referencing other tokens.
7391
* Provides a resolve method to recursively resolve tokens and aliases into their final string value.
7492
* Utilizes caching for efficient resolution of repeated token lookups.
7593
* Integrates with a generic registry system (useRegistry) for management of tokens by ID.
7694

7795
* **Parameters**
7896
* tokens (optional): A nested collection of tokens to initialize the registry.
97+
* options (optional):
98+
* depth?: numberhow deep to flatten nested objects (default: Infinity)
99+
* prefix?: stringa prefix to preprend to all generated ids
79100

80101
* **Returns**
81102
* A token context object containing:

packages/0/src/composables/useTokens/index.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Composables
2-
import { createTokensContext } from './index'
2+
import { createTokensContext, useTokens } from './index'
33

44
// Utilities
55
import { describe, it, expect } from 'vitest'
@@ -73,6 +73,25 @@ describe('createTokensContext', () => {
7373
expect(context.resolve('colors.red.100')).toBe('#FEF2F2')
7474
expect(context.resolve('colors.red.200')).toBe('#FEF2F2')
7575
})
76+
77+
it('should respect depth option (depth=0 stops flattening)', () => {
78+
const tokens: TokenCollection = {
79+
dark: true,
80+
rtl: { value: true, variation: 'toggle' },
81+
complex: { inner: { leaf: '#FFFFFF' } },
82+
}
83+
84+
const context = useTokens(tokens, { depth: 0 })
85+
86+
// With depth=0, nested objects remain as values at their base ids
87+
expect(context.collection.size).toBe(3)
88+
expect(context.collection.has('dark')).toBe(true)
89+
expect(context.collection.has('rtl')).toBe(true)
90+
expect(context.collection.has('complex')).toBe(true)
91+
92+
expect(context.resolve('rtl')).toEqual({ value: true, variation: 'toggle' })
93+
expect(context.resolve('complex')).toEqual({ inner: { leaf: '#FFFFFF' } })
94+
})
7695
})
7796

7897
describe('alias resolution', () => {

packages/0/src/composables/useTokens/index.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export interface TokenContext<Z extends TokenTicket> extends RegistryContext<Z>
4242
resolve: (token: string | TokenAlias) => unknown | undefined
4343
}
4444

45+
export interface TokenOptions {
46+
depth?: number
47+
prefix?: string
48+
}
49+
4550
/**
4651
* Creates a token registry for managing design token collections with alias resolution.
4752
* Returns the token context directly for simple usage.
@@ -50,18 +55,23 @@ export interface TokenContext<Z extends TokenTicket> extends RegistryContext<Z>
5055
* @template Z The structure of the registry token items.
5156
* @template E The available methods for the token's context.
5257
* @returns The token context object.
58+
*
59+
* @see https://0.vuetifyjs.com/composables/registration/use-tokens
5360
* @see https://www.designtokens.org/tr/drafts/format/
5461
*/
5562
export function useTokens<
5663
Z extends TokenTicket = TokenTicket,
5764
E extends TokenContext<Z> = TokenContext<Z>,
58-
> (tokens: TokenCollection = {}): E {
65+
> (
66+
tokens: TokenCollection = {},
67+
options: TokenOptions = {},
68+
): E {
5969
const logger = useLogger()
6070
const registry = useRegistry<Z, E>()
6171

6272
const cache = new Map<string, unknown | undefined>()
6373

64-
registry.onboard(flatten(tokens) as Partial<Z>[])
74+
registry.onboard(flatten(tokens, options.prefix, options.depth) as Partial<Z>[])
6575

6676
function isAlias (token: unknown): token is string {
6777
return isString(token) && token.length > 2 && token[0] === '{' && token.at(-1) === '}'
@@ -185,12 +195,12 @@ export function createTokensContext<
185195
* @param prefix An optional prefix to prepend to each token ID.
186196
* @returns An array of flattened tokens, each with an ID and value.
187197
*/
188-
function flatten (tokens: TokenCollection, prefix = ''): FlatTokenCollection[] {
198+
function flatten (tokens: TokenCollection, prefix = '', depth = Infinity): FlatTokenCollection[] {
189199
const flattened: FlatTokenCollection[] = []
190-
const stack: Array<{ tokens: TokenCollection, prefix: string }> = [{ tokens, prefix }]
200+
const stack: { tokens: TokenCollection, prefix: string, depth: number }[] = [{ tokens, prefix, depth }]
191201

192202
while (stack.length > 0) {
193-
const { tokens: currentTokens, prefix: currentPrefix } = stack.pop()!
203+
const { tokens: currentTokens, prefix: currentPrefix, depth: currentDepth } = stack.pop()!
194204

195205
const meta: Record<string, unknown> = {}
196206
for (const k in currentTokens) {
@@ -216,7 +226,7 @@ function flatten (tokens: TokenCollection, prefix = ''): FlatTokenCollection[] {
216226
flattened.push({ id, value: value as TokenAlias })
217227

218228
const inner = value.$value
219-
if (isObject(inner)) {
229+
if (isObject(inner) && currentDepth > 0) {
220230
for (const innerKey in inner) {
221231
if (innerKey.startsWith('$')) continue
222232

@@ -227,14 +237,19 @@ function flatten (tokens: TokenCollection, prefix = ''): FlatTokenCollection[] {
227237
} else if ('$value' in child) {
228238
flattened.push({ id: childId, value: child as TokenAlias })
229239
} else {
230-
stack.push({ tokens: child as TokenCollection, prefix: childId })
240+
stack.push({ tokens: child as TokenCollection, prefix: childId, depth: currentDepth - 1 })
231241
}
232242
}
233243
}
234244
continue
235245
}
236246

237-
stack.push({ tokens: value as TokenCollection, prefix: id })
247+
if (currentDepth <= 0) {
248+
flattened.push({ id, value: value as unknown as TokenValue })
249+
continue
250+
}
251+
252+
stack.push({ tokens: value as TokenCollection, prefix: id, depth: currentDepth - 1 })
238253
}
239254
}
240255

0 commit comments

Comments
 (0)