@karyl-chan/ui 0.2.0 → 0.4.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.
- package/package.json +1 -1
- package/src/components/AppConfirmDialog.vue +2 -4
- package/src/components/AppModal.vue +16 -7
- package/src/components/AppTabs.vue +40 -3
- package/src/components/AppTabsRouted.vue +33 -3
- package/src/components/Cluster.vue +59 -0
- package/src/components/Spacer.vue +23 -0
- package/src/components/Stack.vue +70 -0
- package/src/index.ts +13 -0
- package/src/lib/space.ts +44 -0
- package/src/tokens.css +15 -0
package/package.json
CHANGED
|
@@ -85,11 +85,9 @@ const emit = defineEmits<{
|
|
|
85
85
|
</template>
|
|
86
86
|
|
|
87
87
|
<style scoped>
|
|
88
|
-
/* Inner wrapper:
|
|
89
|
-
AppModal's body
|
|
90
|
-
component is responsible for its own spacing. */
|
|
88
|
+
/* Inner wrapper: flex-column layout + gap. Padding comes from
|
|
89
|
+
AppModal's body default. */
|
|
91
90
|
.acd-body {
|
|
92
|
-
padding: 0.9rem 1rem 0.75rem;
|
|
93
91
|
display: flex;
|
|
94
92
|
flex-direction: column;
|
|
95
93
|
gap: 0.75rem;
|
|
@@ -13,7 +13,11 @@ import { useDrawer } from '../composables/use-drawer';
|
|
|
13
13
|
* The caller owns everything inside the slot — typical usage is to drop
|
|
14
14
|
* a `<form @submit.prevent>` containing fields + an actions footer
|
|
15
15
|
* directly into the default slot. AppModal supplies the chrome (backdrop,
|
|
16
|
-
* panel, optional title header, close button)
|
|
16
|
+
* panel, optional title header, close button) and pads the body with
|
|
17
|
+
* `padding` (default `0.8rem 0.9rem`, both viewport branches). Slot
|
|
18
|
+
* content therefore needs no padding of its own; pass `padding="0"` for
|
|
19
|
+
* full-bleed content (tables, pickers, croppers) that manages its own
|
|
20
|
+
* edges.
|
|
17
21
|
*
|
|
18
22
|
* `closeOnBackdrop` / `closeOnEscape` default to true. Escape closure on
|
|
19
23
|
* desktop is wired locally; on mobile it's handled by useDrawer's escape
|
|
@@ -27,11 +31,17 @@ const props = withDefaults(defineProps<{
|
|
|
27
31
|
closeOnEscape?: boolean;
|
|
28
32
|
/** Width of the desktop panel; mobile drawer ignores this. */
|
|
29
33
|
width?: string;
|
|
34
|
+
/**
|
|
35
|
+
* CSS padding of the scrollable body around the default slot. Use
|
|
36
|
+
* `'0'` for full-bleed content that manages its own edges.
|
|
37
|
+
*/
|
|
38
|
+
padding?: string;
|
|
30
39
|
}>(), {
|
|
31
40
|
title: '',
|
|
32
41
|
closeOnBackdrop: true,
|
|
33
42
|
closeOnEscape: true,
|
|
34
|
-
width: 'min(440px, 92vw)'
|
|
43
|
+
width: 'min(440px, 92vw)',
|
|
44
|
+
padding: '0.8rem 0.9rem'
|
|
35
45
|
});
|
|
36
46
|
|
|
37
47
|
const emit = defineEmits<{
|
|
@@ -101,7 +111,7 @@ function onBackdropClick() {
|
|
|
101
111
|
<Icon icon="material-symbols:close-rounded" width="18" height="18" />
|
|
102
112
|
</button>
|
|
103
113
|
</header>
|
|
104
|
-
<div class="app-modal-body">
|
|
114
|
+
<div class="app-modal-body" :style="{ padding }">
|
|
105
115
|
<slot />
|
|
106
116
|
</div>
|
|
107
117
|
</div>
|
|
@@ -141,7 +151,7 @@ function onBackdropClick() {
|
|
|
141
151
|
<Icon icon="material-symbols:close-rounded" width="18" height="18" />
|
|
142
152
|
</button>
|
|
143
153
|
</header>
|
|
144
|
-
<div class="app-modal-drawer-body">
|
|
154
|
+
<div class="app-modal-drawer-body" :style="{ padding }">
|
|
145
155
|
<slot />
|
|
146
156
|
</div>
|
|
147
157
|
</div>
|
|
@@ -245,9 +255,8 @@ function onBackdropClick() {
|
|
|
245
255
|
|
|
246
256
|
/* ── Desktop body wrapper ───────────────────────────────────────── */
|
|
247
257
|
/* Structural wrapper that matches the mobile .app-modal-drawer-body.
|
|
248
|
-
Padding
|
|
249
|
-
|
|
250
|
-
supply their own inner padding. */
|
|
258
|
+
Padding comes from the `padding` prop (inline style) on both
|
|
259
|
+
branches so slot content needs none of its own. */
|
|
251
260
|
.app-modal-body {
|
|
252
261
|
flex: 1;
|
|
253
262
|
min-height: 0;
|
|
@@ -32,10 +32,19 @@ const props = withDefaults(defineProps<{
|
|
|
32
32
|
subTabs?: TabDef[];
|
|
33
33
|
/** Where sub-tabs render on desktop. Has no effect on mobile. */
|
|
34
34
|
subLayout?: 'top' | 'sidebar';
|
|
35
|
+
/**
|
|
36
|
+
* Primary-tab appearance (desktop only — mobile always uses the
|
|
37
|
+
* dropdown). `'pill'` (default) = filled/bordered pill buttons;
|
|
38
|
+
* `'underline'` = borderless tabs with an accent underline on the
|
|
39
|
+
* active one (the quieter, header-strip look). Sub-tabs are always
|
|
40
|
+
* underline-styled regardless.
|
|
41
|
+
*/
|
|
42
|
+
variant?: 'pill' | 'underline';
|
|
35
43
|
}>(), {
|
|
36
44
|
subModelValue: undefined,
|
|
37
45
|
subTabs: () => [],
|
|
38
|
-
subLayout: 'top'
|
|
46
|
+
subLayout: 'top',
|
|
47
|
+
variant: 'pill'
|
|
39
48
|
});
|
|
40
49
|
|
|
41
50
|
const emit = defineEmits<{
|
|
@@ -82,7 +91,7 @@ const subOptions = computed<SelectOption<string>[]>(() =>
|
|
|
82
91
|
<!-- Desktop, sub-tabs below primary. -->
|
|
83
92
|
<div
|
|
84
93
|
v-else-if="!hasSub || subLayout === 'top'"
|
|
85
|
-
class="tabs-root desktop top"
|
|
94
|
+
:class="['tabs-root', 'desktop', 'top', { underline: variant === 'underline' }]"
|
|
86
95
|
>
|
|
87
96
|
<nav class="primary-row" role="tablist">
|
|
88
97
|
<button
|
|
@@ -120,7 +129,7 @@ const subOptions = computed<SelectOption<string>[]>(() =>
|
|
|
120
129
|
</div>
|
|
121
130
|
|
|
122
131
|
<!-- Desktop, sub-tabs as a vertical sidebar. -->
|
|
123
|
-
<div v-else class="tabs-root desktop sidebar">
|
|
132
|
+
<div v-else :class="['tabs-root', 'desktop', 'sidebar', { underline: variant === 'underline' }]">
|
|
124
133
|
<nav class="primary-row" role="tablist">
|
|
125
134
|
<button
|
|
126
135
|
v-for="t in tabs"
|
|
@@ -213,6 +222,34 @@ const subOptions = computed<SelectOption<string>[]>(() =>
|
|
|
213
222
|
}
|
|
214
223
|
.tab:disabled { opacity: 0.45; cursor: default; }
|
|
215
224
|
|
|
225
|
+
/* Underline variant — borderless primary tabs with an accent underline on
|
|
226
|
+
the active one (mirrors the plugin-aktest nav strip and the sub-tab
|
|
227
|
+
treatment). Railless: the row drops its bottom border so each tab owns
|
|
228
|
+
its own indicator. */
|
|
229
|
+
.tabs-root.underline .primary-row {
|
|
230
|
+
border-bottom: none;
|
|
231
|
+
padding-bottom: 0;
|
|
232
|
+
gap: 0.15rem;
|
|
233
|
+
}
|
|
234
|
+
.tabs-root.underline .tab {
|
|
235
|
+
background: none;
|
|
236
|
+
border: none;
|
|
237
|
+
border-radius: 0;
|
|
238
|
+
border-bottom: 2px solid transparent;
|
|
239
|
+
color: var(--text-muted);
|
|
240
|
+
padding: 0.45rem 0.6rem;
|
|
241
|
+
}
|
|
242
|
+
.tabs-root.underline .tab:hover:not(:disabled) {
|
|
243
|
+
background: none;
|
|
244
|
+
color: var(--text);
|
|
245
|
+
}
|
|
246
|
+
.tabs-root.underline .tab.active {
|
|
247
|
+
background: none;
|
|
248
|
+
border-color: transparent;
|
|
249
|
+
border-bottom-color: var(--accent);
|
|
250
|
+
color: var(--accent-text-strong);
|
|
251
|
+
}
|
|
252
|
+
|
|
216
253
|
.sub-row {
|
|
217
254
|
display: flex;
|
|
218
255
|
gap: 0.6rem;
|
|
@@ -36,11 +36,16 @@ const props = withDefaults(defineProps<{
|
|
|
36
36
|
* routed AppTabs share a page. Empty (default) = plain `tab` /
|
|
37
37
|
* `sub`; non-empty = `${name}-tab` / `${name}-sub`. */
|
|
38
38
|
name?: string;
|
|
39
|
+
/** Primary-tab appearance (desktop only). `'pill'` (default) = filled
|
|
40
|
+
* pill buttons; `'underline'` = borderless with an accent underline on
|
|
41
|
+
* the active tab. Matches AppTabs' `variant`. */
|
|
42
|
+
variant?: 'pill' | 'underline';
|
|
39
43
|
}>(), {
|
|
40
44
|
subModelValue: undefined,
|
|
41
45
|
subTabs: () => [],
|
|
42
46
|
subLayout: 'top',
|
|
43
|
-
name: ''
|
|
47
|
+
name: '',
|
|
48
|
+
variant: 'pill'
|
|
44
49
|
});
|
|
45
50
|
|
|
46
51
|
const emit = defineEmits<{
|
|
@@ -154,7 +159,7 @@ function subLinkTo(key: string) {
|
|
|
154
159
|
<!-- Desktop, sub-tabs below primary. -->
|
|
155
160
|
<div
|
|
156
161
|
v-else-if="!hasSub || subLayout === 'top'"
|
|
157
|
-
class="tabs-root desktop top"
|
|
162
|
+
:class="['tabs-root', 'desktop', 'top', { underline: variant === 'underline' }]"
|
|
158
163
|
>
|
|
159
164
|
<nav class="primary-row" role="tablist">
|
|
160
165
|
<template v-for="t in tabs" :key="t.key">
|
|
@@ -212,7 +217,7 @@ function subLinkTo(key: string) {
|
|
|
212
217
|
</div>
|
|
213
218
|
|
|
214
219
|
<!-- Desktop, sub-tabs as a vertical sidebar. -->
|
|
215
|
-
<div v-else class="tabs-root desktop sidebar">
|
|
220
|
+
<div v-else :class="['tabs-root', 'desktop', 'sidebar', { underline: variant === 'underline' }]">
|
|
216
221
|
<nav class="primary-row" role="tablist">
|
|
217
222
|
<template v-for="t in tabs" :key="t.key">
|
|
218
223
|
<RouterLink
|
|
@@ -313,6 +318,31 @@ function subLinkTo(key: string) {
|
|
|
313
318
|
}
|
|
314
319
|
.tab:disabled { opacity: 0.45; cursor: default; }
|
|
315
320
|
|
|
321
|
+
/* Underline variant — see AppTabs.vue. Kept in sync with its plain sibling. */
|
|
322
|
+
.tabs-root.underline .primary-row {
|
|
323
|
+
border-bottom: none;
|
|
324
|
+
padding-bottom: 0;
|
|
325
|
+
gap: 0.15rem;
|
|
326
|
+
}
|
|
327
|
+
.tabs-root.underline .tab {
|
|
328
|
+
background: none;
|
|
329
|
+
border: none;
|
|
330
|
+
border-radius: 0;
|
|
331
|
+
border-bottom: 2px solid transparent;
|
|
332
|
+
color: var(--text-muted);
|
|
333
|
+
padding: 0.45rem 0.6rem;
|
|
334
|
+
}
|
|
335
|
+
.tabs-root.underline .tab:hover:not(:disabled) {
|
|
336
|
+
background: none;
|
|
337
|
+
color: var(--text);
|
|
338
|
+
}
|
|
339
|
+
.tabs-root.underline .tab.active {
|
|
340
|
+
background: none;
|
|
341
|
+
border-color: transparent;
|
|
342
|
+
border-bottom-color: var(--accent);
|
|
343
|
+
color: var(--accent-text-strong);
|
|
344
|
+
}
|
|
345
|
+
|
|
316
346
|
.sub-row {
|
|
317
347
|
display: flex;
|
|
318
348
|
gap: 0.6rem;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Cluster — a horizontal group that wraps, owning the gap between items.
|
|
4
|
+
*
|
|
5
|
+
* For toolbars, tag/badge rows, button groups — anywhere items flow
|
|
6
|
+
* horizontally and should wrap rather than overflow. Defaults to
|
|
7
|
+
* vertically-centered, wrapping items. Pair with <Spacer /> to push a
|
|
8
|
+
* trailing item to the end instead of a one-off `margin-left:auto`:
|
|
9
|
+
*
|
|
10
|
+
* <Cluster>
|
|
11
|
+
* <h2>Title</h2>
|
|
12
|
+
* <Spacer />
|
|
13
|
+
* <AppButton>Action</AppButton>
|
|
14
|
+
* </Cluster>
|
|
15
|
+
*
|
|
16
|
+
* `gap` takes a spacing-scale step (0–8 → --space-*) or a raw CSS length.
|
|
17
|
+
*/
|
|
18
|
+
import { computed, type CSSProperties } from "vue";
|
|
19
|
+
import {
|
|
20
|
+
resolveSpace,
|
|
21
|
+
ALIGN_ITEMS,
|
|
22
|
+
JUSTIFY_CONTENT,
|
|
23
|
+
type Align,
|
|
24
|
+
type Justify,
|
|
25
|
+
} from "../lib/space";
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(
|
|
28
|
+
defineProps<{
|
|
29
|
+
gap?: string | number;
|
|
30
|
+
align?: Align;
|
|
31
|
+
justify?: Justify;
|
|
32
|
+
/** Wrap onto multiple lines when items don't fit. On by default —
|
|
33
|
+
* that's the whole point of a cluster. */
|
|
34
|
+
wrap?: boolean;
|
|
35
|
+
}>(),
|
|
36
|
+
{
|
|
37
|
+
gap: 3,
|
|
38
|
+
align: "center",
|
|
39
|
+
justify: "start",
|
|
40
|
+
wrap: true,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const style = computed<CSSProperties>(() => ({
|
|
45
|
+
display: "flex",
|
|
46
|
+
flexWrap: props.wrap ? "wrap" : "nowrap",
|
|
47
|
+
gap: resolveSpace(props.gap),
|
|
48
|
+
alignItems: ALIGN_ITEMS[props.align],
|
|
49
|
+
justifyContent: JUSTIFY_CONTENT[props.justify],
|
|
50
|
+
}));
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div class="ui-cluster" :style="style"><slot /></div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<style scoped>
|
|
58
|
+
.ui-cluster { min-width: 0; }
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Spacer — an empty flex-grow filler that pushes its siblings apart
|
|
4
|
+
* inside a Stack / Cluster (or any flex container), replacing the
|
|
5
|
+
* one-off `margin-left:auto` / `margin-top:auto` idiom:
|
|
6
|
+
*
|
|
7
|
+
* <Cluster>
|
|
8
|
+
* <Title />
|
|
9
|
+
* <Spacer /> <!-- eats the free space -->
|
|
10
|
+
* <Actions />
|
|
11
|
+
* </Cluster>
|
|
12
|
+
*
|
|
13
|
+
* Purely presentational, so it's hidden from the accessibility tree.
|
|
14
|
+
*/
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="ui-spacer" aria-hidden="true"></div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<style scoped>
|
|
22
|
+
.ui-spacer { flex: 1 1 auto; }
|
|
23
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Stack — a flex layout that OWNS the gap between its children.
|
|
4
|
+
*
|
|
5
|
+
* Reach for this instead of hand-authoring `display:flex; flex-direction:
|
|
6
|
+
* column; gap:…` (or per-element margins) in a view's scoped CSS — that
|
|
7
|
+
* pattern appears hundreds of times across the app and is exactly the
|
|
8
|
+
* responsibility a layout primitive should carry, not each component.
|
|
9
|
+
* Because the parent Stack owns the spacing, the components inside it
|
|
10
|
+
* never need (or should have) outer margins of their own.
|
|
11
|
+
*
|
|
12
|
+
* <Stack gap="4"> vertical, 1rem gaps (--space-4)
|
|
13
|
+
* <VoiceCard />
|
|
14
|
+
* <AppTabs ... />
|
|
15
|
+
* <section>…</section>
|
|
16
|
+
* </Stack>
|
|
17
|
+
*
|
|
18
|
+
* `gap` takes a spacing-scale step (0–8 → --space-*) or a raw CSS length.
|
|
19
|
+
*/
|
|
20
|
+
import { computed, type CSSProperties } from "vue";
|
|
21
|
+
import {
|
|
22
|
+
resolveSpace,
|
|
23
|
+
ALIGN_ITEMS,
|
|
24
|
+
JUSTIFY_CONTENT,
|
|
25
|
+
type Align,
|
|
26
|
+
type Justify,
|
|
27
|
+
} from "../lib/space";
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(
|
|
30
|
+
defineProps<{
|
|
31
|
+
/** Gap between children: a scale step (0–8) or a raw CSS length. */
|
|
32
|
+
gap?: string | number;
|
|
33
|
+
/** Main-axis direction. Default vertical. */
|
|
34
|
+
direction?: "column" | "row";
|
|
35
|
+
align?: Align;
|
|
36
|
+
justify?: Justify;
|
|
37
|
+
/** Allow children to wrap onto multiple lines. */
|
|
38
|
+
wrap?: boolean;
|
|
39
|
+
/** Render as inline-flex (shrink-to-content) instead of flex. */
|
|
40
|
+
inline?: boolean;
|
|
41
|
+
}>(),
|
|
42
|
+
{
|
|
43
|
+
gap: 4,
|
|
44
|
+
direction: "column",
|
|
45
|
+
align: "stretch",
|
|
46
|
+
justify: "start",
|
|
47
|
+
wrap: false,
|
|
48
|
+
inline: false,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const style = computed<CSSProperties>(() => ({
|
|
53
|
+
display: props.inline ? "inline-flex" : "flex",
|
|
54
|
+
flexDirection: props.direction,
|
|
55
|
+
gap: resolveSpace(props.gap),
|
|
56
|
+
alignItems: ALIGN_ITEMS[props.align],
|
|
57
|
+
justifyContent: JUSTIFY_CONTENT[props.justify],
|
|
58
|
+
flexWrap: props.wrap ? "wrap" : "nowrap",
|
|
59
|
+
}));
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<div class="ui-stack" :style="style"><slot /></div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
/* min-width:0 lets a column stack's children (e.g. text) truncate instead
|
|
68
|
+
of forcing the stack wider than its parent. */
|
|
69
|
+
.ui-stack { min-width: 0; }
|
|
70
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
// import "@karyl-chan/ui/use-drawer.css";
|
|
8
8
|
// import "@karyl-chan/ui/use-popover.css";
|
|
9
9
|
|
|
10
|
+
// ── Layout primitives ────────────────────────────────────────────────────
|
|
11
|
+
// Own the space BETWEEN elements so views (and the components inside them)
|
|
12
|
+
// don't hand-author flex+gap or per-element margins. See lib/space.ts for
|
|
13
|
+
// the `gap` scale.
|
|
14
|
+
export { default as Stack } from "./components/Stack.vue";
|
|
15
|
+
export { default as Cluster } from "./components/Cluster.vue";
|
|
16
|
+
export { default as Spacer } from "./components/Spacer.vue";
|
|
17
|
+
export {
|
|
18
|
+
resolveSpace,
|
|
19
|
+
type Align,
|
|
20
|
+
type Justify,
|
|
21
|
+
} from "./lib/space";
|
|
22
|
+
|
|
10
23
|
// ── Components ───────────────────────────────────────────────────────────
|
|
11
24
|
export { default as AppBadge } from "./components/AppBadge.vue";
|
|
12
25
|
export type { BadgeSize, BadgeTone, BadgeVariant } from "./components/AppBadge.vue";
|
package/src/lib/space.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spacing helpers for the layout primitives (Stack / Cluster / Spacer).
|
|
3
|
+
*
|
|
4
|
+
* `gap` and friends take a spacing-scale STEP (0–8, as a number or
|
|
5
|
+
* string) which maps to the corresponding `--space-*` token defined in
|
|
6
|
+
* tokens.css, or a raw CSS length (e.g. `"2px"`, `"1.5rem"`) as an
|
|
7
|
+
* escape hatch. Keeping the scale in tokens.css means the whole app
|
|
8
|
+
* shares one rhythm instead of 200 hand-picked gap values.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Steps that exist as `--space-N` tokens in tokens.css. */
|
|
12
|
+
const STEPS = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8"]);
|
|
13
|
+
|
|
14
|
+
/** Resolve a gap/space prop to a CSS length: a scale step → `var(--space-N)`,
|
|
15
|
+
* anything else → the value verbatim (raw CSS length escape hatch). */
|
|
16
|
+
export function resolveSpace(
|
|
17
|
+
value: string | number | undefined,
|
|
18
|
+
fallback = "0",
|
|
19
|
+
): string {
|
|
20
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
21
|
+
const s = String(value);
|
|
22
|
+
return STEPS.has(s) ? `var(--space-${s})` : s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type Align = "start" | "center" | "end" | "stretch" | "baseline";
|
|
26
|
+
export type Justify = "start" | "center" | "end" | "between" | "around";
|
|
27
|
+
|
|
28
|
+
/** Friendly `align` prop → CSS `align-items` value. */
|
|
29
|
+
export const ALIGN_ITEMS: Record<Align, string> = {
|
|
30
|
+
start: "flex-start",
|
|
31
|
+
center: "center",
|
|
32
|
+
end: "flex-end",
|
|
33
|
+
stretch: "stretch",
|
|
34
|
+
baseline: "baseline",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Friendly `justify` prop → CSS `justify-content` value. */
|
|
38
|
+
export const JUSTIFY_CONTENT: Record<Justify, string> = {
|
|
39
|
+
start: "flex-start",
|
|
40
|
+
center: "center",
|
|
41
|
+
end: "flex-end",
|
|
42
|
+
between: "space-between",
|
|
43
|
+
around: "space-around",
|
|
44
|
+
};
|
package/src/tokens.css
CHANGED
|
@@ -66,6 +66,21 @@
|
|
|
66
66
|
--radius-lg: 8px;
|
|
67
67
|
--radius-xl: 12px;
|
|
68
68
|
--radius-pill: 999px;
|
|
69
|
+
|
|
70
|
+
/* Spacing scale — the single source of truth for gaps between
|
|
71
|
+
* elements. Consume via the <Stack> / <Cluster> layout primitives
|
|
72
|
+
* (gap="4") rather than hand-authoring margins/gaps per view, so
|
|
73
|
+
* inter-element rhythm is owned by the layout layer, not by each
|
|
74
|
+
* component. Theme-independent (spacing doesn't change light/dark). */
|
|
75
|
+
--space-0: 0;
|
|
76
|
+
--space-1: 0.25rem;
|
|
77
|
+
--space-2: 0.5rem;
|
|
78
|
+
--space-3: 0.75rem;
|
|
79
|
+
--space-4: 1rem;
|
|
80
|
+
--space-5: 1.5rem;
|
|
81
|
+
--space-6: 2rem;
|
|
82
|
+
--space-7: 3rem;
|
|
83
|
+
--space-8: 4rem;
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
/* Pin the UA-side color-scheme hint so native widgets (form controls,
|