diff --git a/apps/dialtone-documentation/docs/.vuepress/baseComponents/ComponentHealthStatusTable.vue b/apps/dialtone-documentation/docs/.vuepress/baseComponents/ComponentHealthStatusTable.vue index 8ca7336d50..b9c72401e5 100644 --- a/apps/dialtone-documentation/docs/.vuepress/baseComponents/ComponentHealthStatusTable.vue +++ b/apps/dialtone-documentation/docs/.vuepress/baseComponents/ComponentHealthStatusTable.vue @@ -2,7 +2,7 @@ @@ -114,7 +114,7 @@ const statusIcon = { 'N/A': 'box', }; const statusColor = { - Ready: 'd-fc-success', + Ready: 'd-fc-positive', 'In progress': 'd-fc-warning', Planned: 'd-fc-critical', 'N/A': 'd-fc-muted', diff --git a/apps/dialtone-documentation/docs/.vuepress/baseComponents/CopyButton.vue b/apps/dialtone-documentation/docs/.vuepress/baseComponents/CopyButton.vue index 6b7f8a6d38..e294203f3d 100644 --- a/apps/dialtone-documentation/docs/.vuepress/baseComponents/CopyButton.vue +++ b/apps/dialtone-documentation/docs/.vuepress/baseComponents/CopyButton.vue @@ -10,7 +10,7 @@ Copy MD diff --git a/apps/dialtone-documentation/docs/_data/site-nav.json b/apps/dialtone-documentation/docs/_data/site-nav.json index 23e73d4e0c..64dec961db 100644 --- a/apps/dialtone-documentation/docs/_data/site-nav.json +++ b/apps/dialtone-documentation/docs/_data/site-nav.json @@ -253,6 +253,10 @@ "text": "Banner", "link": "/components/banner.html" }, + { + "text": "Box", + "link": "/components/box.html" + }, { "text": "Breadcrumbs", "link": "/components/breadcrumbs.html" diff --git a/apps/dialtone-documentation/docs/components/box.md b/apps/dialtone-documentation/docs/components/box.md new file mode 100644 index 0000000000..e3bc1d4b35 --- /dev/null +++ b/apps/dialtone-documentation/docs/components/box.md @@ -0,0 +1,297 @@ +--- +title: Box +description: Low-level surface and spacing primitive for building token-constrained containers. +status: beta +thumb: true +storybook: https://dialtone.dialpad.com/vue/?path=/story/components-box--default +keywords: ["box", "container", "surface", "padding", "border", "shadow", "radius", "sizing", "overflow", "scrollbar", "background", "spacing", "layout", "primitive", "elevation"] +--- + + + +## Usage + +```vue code-only + ... +``` + +DtBox complements [DtText](/components/text.html) (typography) and [DtStack](/components/stack.html) (flex layout) to form Dialtone's **primitive triad**: + +* **DtBox**: what the container *is* (`surface`, `border`, `padding`, `sizing`) +* **DtText**: what the content *looks* like (`font`, `size`, `color`) +* **DtStack**: how children are *arranged* (`direction`, `gap`, `alignment`) + +### Guidance + +* Use DtBox in place of composing multiple surface utility classes (e.g., `d-bgc-*`, `d-bc-*`, `d-bar*`, `d-bs-*`, `d-p-*`). +* Use the `as` prop to render semantic HTML elements (`section`, `nav`, `article`, `header`, etc.) for accessibility. +* DtBox is a **passive container** — it does not handle layout (use [DtStack](/components/stack.html)) or typography (use [DtText](/components/text.html)). +* Compose DtBox + DtStack + DtText together for structured UI surface containers. + + + + + + +## Surface + +Background surface color mapped to `--dt-color-surface-*` tokens. + +```vue demo + + + primary + secondary + moderate + bold + strong + + +... +``` + +### Semantic surfaces + +```vue demo + + + brand + info + positive + warning + critical + + +... +``` + +## Padding + +Spacing token scale values for internal whitespace. The padding cascade resolves specific sides over axis shorthands over the all-sides shorthand. + +```vue demo + + + padding="200" + padding="300" + padding="400" + + +... +``` + +### Directional padding + +Override specific sides. The cascade resolves: `paddingBlockStart` > `paddingBlock` > `padding`. + +```vue demo + + + All sides: 200 + Inline override: 400 + Block-start override: 400 + +``` + +## Border + +### Border width + +No visible border until a `border-width` is set. Uniform width applies to all sides. + +```vue demo + + + No border + 100 + 200 + +``` + +### Directional border width + +Show borders on specific sides only. + +```vue demo + + + Bottom only + Start critical + Block only + +``` + +### Border color + +Defaults to `'default'` (`--dt-color-border-default`). Only visible when a `border-width` is set. + +```vue demo + + + default + subtle + critical + positive + +``` + +### Border radius + +```vue demo + + + 0 + 200 + 400 + pill + circle + +``` + +## Shadow + +```vue demo + + + small + medium + large + card + +``` + +## Sizing + +Maps to Dialtone's **layout token scale** (`--dt-layout-*`). Supports both fixed values and percentage tokens. + +```vue demo + + + 300 (192px) + 500 (320px) + 50-percent + +``` + +## Overflow + +```vue demo + + Tall content clipped by overflow="hidden". Nemo rem ullam culpa ut laudantium repellat unde. Consequuntur cupiditate voluptatem velit rerum doloremque voluptatum commodi vitae vel inventore iusto ducimus iure? Ex fugit quae iste perferendis eaque! Alias in reiciendis suscipit facere incidunt repellendus! Voluptatibus iste nesciunt numquam consectetur suscipit unde atque tempora saepe est illum quaerat sit natus mollitia excepturi? Repellendus explicabo deserunt ipsam sint esse ab delectus beatae eligendi velit libero quasi culpa ut tenetur sunt corrupti iure suscipit magni fuga blanditiis nihil incidunt! Mollitia voluptas sed temporibus quasi. + +``` + +## Scrollbar + +Integrates the `v-dt-scrollbar` directive. An inner viewport wrapper is inserted automatically, solving the [Custom Scrollbar's](/components/scrollbar.html) single-child constraint. + +```vue demo + + + Scrollable item {{ i }} + + +``` + +## Render as + +Use the `as` prop to render semantic HTML elements for accessibility. + +```vue demo + + + as="section" + as="nav" + as="article" + +``` + +## Examples + +### Card + +```vue demo + + + Card title + Card body content with supporting text. + + +``` + +### Composed layout + +```vue demo + + + + Title + Action + + + + Box 1 + + + Box 2 + + + Box 3 + + + + +``` + +## Accessibility + +* Use the `as` prop to render appropriate semantic elements — `nav` for navigation, `section` for thematic content, `article` for self-contained content. +* DtBox does not add any implicit ARIA role. The rendered element's native semantics determine how screen readers interpret it. +* When using `as="nav"` or `as="section"`, consider adding `aria-label` to provide an accessible name for the landmark region. +* The scrollbar integration preserves native keyboard scrolling behavior. + +## Vue API + + + +## Classes + + diff --git a/apps/dialtone-documentation/docs/components/description-list.md b/apps/dialtone-documentation/docs/components/description-list.md index 53cf26b812..413a615239 100644 --- a/apps/dialtone-documentation/docs/components/description-list.md +++ b/apps/dialtone-documentation/docs/components/description-list.md @@ -61,7 +61,7 @@ keywords: ["definition list", "key value", "dl", "d-description-list", "DtDescri :items="items" direction="row" :termClass="[`d-fc-critical`, `d-fw-bold`]" - :descriptionClass="[`d-fc-success`]" + :descriptionClass="[`d-fc-positive`]" /> ``` diff --git a/apps/dialtone-documentation/docs/components/icon.md b/apps/dialtone-documentation/docs/components/icon.md index d6a92a2003..1bd08b7d90 100644 --- a/apps/dialtone-documentation/docs/components/icon.md +++ b/apps/dialtone-documentation/docs/components/icon.md @@ -80,7 +80,7 @@ The icon's color inherits from the parent's foreground color. ``` ```html - + Settings @@ -101,8 +101,8 @@ When setting the color of an icon take these into consideration: Ai Contact Center - - Available + + Available @@ -121,7 +121,7 @@ When setting the color of an icon take these into consideration: - Available + Available @@ -214,7 +214,7 @@ Dialtone provides eight sizes for icons. Each of the sizes represents the width ]; const iconColors = [ - { value: 'd-fc-success', label: 'd-fc-success' }, + { value: 'd-fc-positive', label: 'd-fc-positive' }, { value: 'd-fc-critical', label: 'd-fc-critical' }, { value: 'd-fc-primary', label: 'd-fc-primary' }, ]; @@ -226,7 +226,7 @@ Dialtone provides eight sizes for icons. Each of the sizes represents the width const selectedIcon = ref('settings'); const selectedSize = ref('500'); - const selectedColor = ref('d-fc-success'); + const selectedColor = ref('d-fc-positive'); const selectedDirection = ref('row'); diff --git a/apps/dialtone-documentation/docs/components/mode-island.md b/apps/dialtone-documentation/docs/components/mode-island.md index 771387f9b7..e0c399457d 100644 --- a/apps/dialtone-documentation/docs/components/mode-island.md +++ b/apps/dialtone-documentation/docs/components/mode-island.md @@ -113,7 +113,7 @@ keywords: ["theme island","mode override","v-dt-mode","directive","light","dark" Inverted (auto) - + Primary Muted Critical @@ -129,7 +129,7 @@ keywords: ["theme island","mode override","v-dt-mode","directive","light","dark" Explicit light - + Primary Muted Critical @@ -145,7 +145,7 @@ keywords: ["theme island","mode override","v-dt-mode","directive","light","dark" Explicit dark - + Primary Muted Critical @@ -170,9 +170,9 @@ Use the `v-dt-mode` directive to control the color mode of a region, component, ```vue demo - Dark content - Light content - Inverted — opposite of parent or root + Dark content + Light content + Inverted — opposite of parent or root ``` @@ -216,10 +216,10 @@ Bind a reactive variable as the directive arg to switch modes at runtime. Dark - {{ dynamicMode }} mode + {{ dynamicMode }} mode - ... mode + ... mode ``` ### Conditional @@ -268,7 +268,7 @@ The default mode — inverts relative to the nearest parent mode boundary or the ```vue demo
- Inverted mode (opposite of parent) + Inverted mode (opposite of parent)
``` @@ -278,7 +278,7 @@ Explicitly set to light mode regardless of parent or root mode. ```vue demo
- Always light mode + Always light mode
``` @@ -288,7 +288,7 @@ Explicitly set to dark mode regardless of parent or root mode. ```vue demo
- Always dark mode + Always dark mode
``` @@ -298,11 +298,11 @@ Mode boundaries can be nested. Each `v-dt-mode:invert` reads the nearest parent ```vue demo - Explicit Light + Explicit Light - Inverted (Dark) + Inverted (Dark) - Inverted again (Light) + Inverted again (Light) diff --git a/apps/dialtone-documentation/docs/components/stack.md b/apps/dialtone-documentation/docs/components/stack.md index 919a198223..882c66fc7d 100644 --- a/apps/dialtone-documentation/docs/components/stack.md +++ b/apps/dialtone-documentation/docs/components/stack.md @@ -5,7 +5,7 @@ status: ready thumb: true image: assets/images/components/stack.png storybook: https://dialtone.dialpad.com/vue/?path=/story/components-stack--default -keywords: ["layout", "vertical", "horizontal", "d-stack", "DtStack", "dt-stack", "flex container", "auto layout"] +keywords: ["layout", "vertical", "horizontal", "d-stack", "DtStack", "dt-stack", "flex container", "auto layout", "primitive"] --- @@ -17,25 +17,24 @@ keywords: ["layout", "vertical", "horizontal", "d-stack", "DtStack", "dt-stack", `direction="column"` will flow child items vertically, i.e. top to bottom. It is the default direction and doesn't need to be explictily set. ```vue demo - -
- Stack item 1 -
-
- Stack item 2 -
-
- Stack item 3 -
-
+ + + + Stack item 1 + + + Stack item 2 + + + Stack item 3 + + + -
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
``` @@ -44,87 +43,90 @@ keywords: ["layout", "vertical", "horizontal", "d-stack", "DtStack", "dt-stack", `direction="row"` will flow child items horizontally, i.e. left to right. ```vue demo - -
- Stack item 1 -
-
- Stack item 2 -
-
- Stack item 3 -
-
+ + + + Stack item 1 + + + Stack item 2 + + + Stack item 3 + + + -
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
``` ### Row Reverse ```vue demo - -
- Stack item 1 -
-
- Stack item 2 -
-
- Stack item 3 -
-
+ + + + Stack item 1 + + + Stack item 2 + + + Stack item 3 + + + -
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
``` ### Column Reverse ```vue demo - -
- Stack item 1 -
-
- Stack item 2 -
-
- Stack item 3 -
-
+ + + + Stack item 1 + + + Stack item 2 + + + Stack item 3 + + + -
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
``` @@ -141,23 +143,24 @@ Declaring as an appropriate HTML element improves accessibility by helping scree Use `as="section"` to create a thematic grouping of content. ```vue demo - -
Stack item 1
-
Stack item 2
-
Stack item 3
-
+ + + Stack item 1 + Stack item 2 + Stack item 3 + + -
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
``` @@ -166,25 +169,26 @@ Use `as="section"` to create a thematic grouping of content. Use `as="span"` when you need an inline container. ```vue demo - - Inline item 1 - Inline item 2
with a second line
- Inline item 3 -
+ + + Inline item 1 + Inline item 2
with a second line
+ Inline item 3 +
+
- Inline item 1 - Inline item 2 - Inline item 3 + Inline item 1 + Inline item 2 + Inline item 3 ``` @@ -222,9 +226,9 @@ Use `as="span"` when you need an inline container. :gap="selectedGap" class="d-bgc-moderate-opaque d-t d-td300 d-bar8 d-ttf-quint" > -
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
@@ -234,25 +238,25 @@ Use `as="span"` when you need an inline container. :gap="selectedGap" class="d-bgc-moderate-opaque d-t d-td300 d-bar8 d-ttf-quint" > -
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
-
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
``` ### Available gaps -
+ @@ -279,7 +283,7 @@ Use `as="span"` when you need an inline container.
-
+
## Align @@ -301,50 +305,50 @@ The `align` prop is optional. Unless specified, it will default vertical stacks gap="100" class="d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--inline-stretch" > -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+ -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+
-
Short
-
+ Short + Taller item
with more content -
-
Short
+ + Short
-
Short
-
+ Short + Taller item
with more content -
-
Short
+ + Short
``` @@ -362,16 +366,16 @@ Align items to the start of the cross-axis. align="start" class="d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--inline-start" > -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+ -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+
@@ -396,24 +400,24 @@ Align items to the start of the cross-axis. gap="100" align="start" > -
Short
-
+ Short + Taller item
with more content -
-
Short
+ + Short -
Short
-
+ Short + Taller item
with more content -
-
Short
+ + Short
``` @@ -431,16 +435,16 @@ Center items along the cross-axis. align="center" class="d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--inline-center" > -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+ -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+
@@ -466,12 +470,12 @@ Center items along the cross-axis. gap="100" align="center" > -
Short
-
+ Short + Taller item
with more content -
-
Short
+ + Short ``` @@ -489,16 +493,16 @@ Align items to the end of the cross-axis. align="end" class="d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--inline-end" > -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+ -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+
@@ -523,24 +527,24 @@ Align items to the end of the cross-axis. gap="100" align="end" > -
Short
-
+ Short + Taller item
with more content -
-
Short
+ + Short -
Short
-
+ Short + Taller item
with more content -
-
Short
+ + Short
``` @@ -558,16 +562,16 @@ Stretch items to fill the container height. align="stretch" class="d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--inline-stretch" > -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+ -
+ Short -
-
+ + Taller item
with more content -
-
+ + Short -
+
@@ -593,12 +597,12 @@ Stretch items to fill the container height. gap="100" align="stretch" > -
Short
-
+ Short + Taller item
with more content -
-
Short
+ + Short ``` @@ -613,15 +617,15 @@ Align items along their text baselines. align="baseline" class="d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--baseline" > -
+ Small body -
-
+ + Medium body -
-
+ + Large headline -
+ -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
-
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
-
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
``` @@ -695,9 +699,9 @@ Center items along the main axis. justify="center" class="d-w100p d-h-400 d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--block-center" > -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3 -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
-
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
``` @@ -733,9 +737,9 @@ Align items to the end of the main axis. justify="end" class="d-w100p d-h-400 d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--block-end" > -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3 -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
-
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
``` @@ -771,9 +775,9 @@ Distribute items with equal space around each item. justify="space-around" class="d-w100p d-h-400 d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--block-center" > -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3 -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
-
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
``` @@ -809,9 +813,9 @@ Distribute items with space between them, edges flush to container. justify="space-between" class="d-w100p d-h-400 d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--block-center" > -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3 -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
@@ -831,9 +835,9 @@ Distribute items with space between them, edges flush to container. justify="space-between" class="d-w100p" > -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3 ``` @@ -852,9 +856,9 @@ Distribute items with equal space between all items, including edges. justify="space-evenly" class="d-w100p d-h-400 d-bgc-moderate-opaque d-bar8 axis-outline axis-outline--block-center" > -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3 -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
-
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
``` @@ -887,30 +891,30 @@ Stacks column at small screen size and column reverse at large screen gap="100" class="d-bgc-moderate-opaque" > -
+ Stack item 1 -
-
+ + Stack item 2 -
-
+ + Stack item 3 -
+ -
+ Stack item 1 -
-
+ + Stack item 2 -
-
+ + Stack item 3 -
+
``` @@ -921,21 +925,21 @@ Set `200` as the default gap, `300` for small and larger, `400` for medium, `500 :gap="{ default: '200', xl: '600', lg: '500', md: '400', sm: '300' }" class="d-bgc-moderate-opaque" > -
+ Stack item 1 -
-
+ + Stack item 2 -
-
+ + Stack item 3 -
+ -
Stack item 1
-
Stack item 2
-
Stack item 3
+ Stack item 1 + Stack item 2 + Stack item 3
``` @@ -944,26 +948,20 @@ Set `200` as the default gap, `300` for small and larger, `400` for medium, `500 Stacks row with gap 500 and stacks in row reverse the nested stack with gap 500. ```vue demo - - - Stack item 1 - - -
Stack item 2
- -
Stack item 3
with multiple lines
-
Stack item 4
+ + + + Stack item 1 + + + Stack item 2 + + Stack item 3
with multiple lines
+ Stack item 4 +
-
+ -
Stack item 2
+ Stack item 2 -
Stack item 3
with multiple lines
-
Stack item 4
+ Stack item 3
with multiple lines
+ Stack item 4
@@ -991,35 +989,36 @@ Stacks row with gap 500 and stacks in row reverse the nested stack with gap 500. Like `direction` and `gap`, the `align` and `justify` props support responsive object syntax to change alignment at different breakpoints. ```vue demo - + -
+ > + Short -
-
+ + Taller
item -
-
+ + Short -
+
+ -
Short
-
+ Short + Taller
item -
-
Short
+ + Short
``` @@ -1032,9 +1031,9 @@ Resize your browser to see the alignment change at different breakpoints. :justify="{ default: 'start', md: 'center', lg: 'space-between' }" class="d-w100p d-bgc-moderate-opaque d-bar8" > -
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
-
Item 1
-
Item 2
-
Item 3
+ Item 1 + Item 2 + Item 3
``` @@ -1068,7 +1067,7 @@ View the [Migrating from Flex CSS Utilities to DtStack](/about/whats-new/posts/2 Katie Rodriguez - + Available diff --git a/apps/dialtone-documentation/docs/components/text.md b/apps/dialtone-documentation/docs/components/text.md index d17649dc3e..bec96df37e 100644 --- a/apps/dialtone-documentation/docs/components/text.md +++ b/apps/dialtone-documentation/docs/components/text.md @@ -4,6 +4,7 @@ description: Consistent typography styling through semantic text kinds and sizes status: ready storybook: https://dialtone.dialpad.com/vue/?path=/story/components-text--default image: assets/images/components/text.png +keywords: ["dt-box", "DtBox", "typography", "size", "tone", "font", "font size", "font weight", "line height", "density", "strength", "primitive"] --- @@ -104,7 +105,7 @@ The `numeric` prop applies styles that ensure that each number is set with consi (886) 555-8888 - With numeric + With numeric (913) 555-3170 (908) 555-1111 (805) 555-8413 @@ -161,8 +162,8 @@ Use `tone` to declare the text's tone, which will map to a foreground color. By muted disabled placeholder - success - success-strong + positive + positive-strong warning critical critical-strong @@ -380,7 +381,7 @@ Text box trim will only affect elements with block or inline-block styled contex Katie Rodriguez - + Available diff --git a/apps/dialtone-documentation/docs/dialtone/whats-new/posts/2026-2-18.md b/apps/dialtone-documentation/docs/dialtone/whats-new/posts/2026-2-18.md index 3eb336fa54..460416bca4 100644 --- a/apps/dialtone-documentation/docs/dialtone/whats-new/posts/2026-2-18.md +++ b/apps/dialtone-documentation/docs/dialtone/whats-new/posts/2026-2-18.md @@ -317,8 +317,8 @@ The tool leaves unmapped base colors unchanged. Dialtone's ESLint rule (`depreca | `black-900` | `d-fc-primary` | `var(--dt-color-foreground-primary)` | | `red-600` | `d-fc-critical` | `var(--dt-color-foreground-critical)` | | `red-700` | `d-fc-critical-strong` | `var(--dt-color-foreground-critical-strong)` | -| `green-800` | `d-fc-success` | `var(--dt-color-foreground-success)` | -| `green-900` | `d-fc-success-strong` | `var(--dt-color-foreground-success-strong)` | +| `green-800` | `d-fc-positive` | `var(--dt-color-foreground-positive)` | +| `green-900` | `d-fc-positive-strong` | `var(--dt-color-foreground-positive-strong)` | | `gold-800` | `d-fc-warning` | `var(--dt-color-foreground-warning)` | ### Surface diff --git a/apps/dialtone-documentation/docs/dialtone/whats-new/posts/2026-4-22.md b/apps/dialtone-documentation/docs/dialtone/whats-new/posts/2026-4-22.md new file mode 100644 index 0000000000..56fb7426de --- /dev/null +++ b/apps/dialtone-documentation/docs/dialtone/whats-new/posts/2026-4-22.md @@ -0,0 +1,138 @@ +--- +heading: 'Introducing DtBox: The Surface and Spacing Primitive' +author: Francis Rupert +posted: '2026-4-22' +excerpt: 'New low-level component for token-constrained surfaces. Replaces manual CSS utility composition for padding, background, border, shadow, radius, and sizing with validated props. Completes the primitive triad alongside DtText and DtStack.' +--- + + + +## TLDR + +- New component [DtBox](/components/box.md) handles surface styling: `padding`, `surface`, `border`, `shadow`, `borderRadius`, `sizing`, `overflow`, and `scrollbar`. +- Completes the **primitive triad**: DtBox (surface) + [DtText](/components/text.md) (typography) + [DtStack](/components/stack.md) (layout). +- Replaces manual composition of CSS utility classes like `class="d-bgc-primary d-bc-default d-baw-100 d-bar-200 d-p-200 d-bs-sm"`. +- All prop values are token-constrained and validated — invalid values produce console warnings, not silent failures. +- [Component docs](/components/box.md) • [Storybook](https://dialtone.dialpad.com/vue/?path=/story/components-box--default) + +## The Primitive Triad + +Dialtone's UI primitives now cover three distinct concerns: + +- **DtBox** — what the container *is*: surface color, border, padding, shadow, sizing +- **DtText** — what the content *looks* like*: font family, size, weight, color +- **DtStack** — how children are *arranged*: flex direction, gap, alignment + +### Demo + +Each primitive owns its domain. In the example below, DtBox provides the visual container (surface, border, shadow, padding). DtStack arranges the children vertically with consistent gap. DtText renders the typography. + +```vue demo + + + Card title + Supporting text that describes the card content. + + +``` + +## Why DtBox Over CSS Utilities? + +Building a custom container previously might look like this: + +```vue demo +
+ Content +
+``` + +Six CSS utility classes from six different naming conventions (`d-bgc-*`, `d-bc-*`, `d-baw-*`, `d-bar-*`, `d-p-*`, `d-bs-*`). Each requires knowing the prefix, the token scale, and the exact naming pattern. A wrong prefix — `d-bg-primary` instead of `d-bgc-primary` — silently fails. + +With DtBox: + +```vue demo + + Content + +``` + +The prop names are self-documenting. The values are validated — pass `surface="foo"` and you get a console warning instead of silent failure. IDE autocomplete works. And the [Dialtone MCP server](/guides/mcp-server/) returns the full prop API in one search, making DtBox significantly easier for AI-assisted development tools to work with. + +> [!INFO] +> DtBox does not replace *all* CSS utilities — just the ones most relevant to container styling. Positioning (`d-ps-sticky`), display (`d-d-none`), flex child (`d-fl1`), and other utilities remain available via the `class` escape hatch. + +## Prop Tiers + +| Tier | Props | Description | +| --- | --- | --- | +| **Core** | `as`, `padding`, `surface`, `borderWidth`, `borderColor`, `borderRadius`, `shadow`, `overflow`, `scrollbar` | The props you'll use most of the time. | +| **Directional** | `paddingBlock`, `paddingBlockStart`, `paddingBlockEnd`, `paddingInline`, `paddingInlineStart`, `paddingInlineEnd`, `borderWidthBlock`, `borderWidthBlockStart`, `borderWidthBlockEnd`, `borderWidthInline`, `borderWidthInlineStart`, `borderWidthInlineEnd` | Per-axis/side overrides. Cascade: specific > axis > shorthand. | +| **Sizing** | `blockSize`, `inlineSize`, `maxBlockSize`, `minBlockSize`, `maxInlineSize`, `minInlineSize` | [Layout token scale](/dialtone/whats-new/posts/2026-3-9.md) (`--dt-layout-*`), including percentages. | + +## Scrollbar Integration + +The `scrollbar` prop integrates the [v-dt-scrollbar](components/scrollbar.md) directive as a first-class feature. + +```vue demo + + + Item {{ i }} + + +``` + +## Composing With the Triad + +Here's a more realistic composition showing how the three primitives of **DtBox**, **DtStack**, and **DtText** work together while each handling exactly one concern: + +```vue demo + + + + Title + Action + + + + Box 1 + + + Box 2 + + + Box 3 + + + + +``` + +- **DtStack** handles the arrangement of elements. +- **DtBox** provides the card surface (`as="aside"` for semantics, padding, border, radius). +- Nested **DtBox** instances provide the secondary surfaces. +- **DtText** handles all typography. +- **class="d-fl1"** is the utility escape hatch for flex-grow — a property DtBox intentionally doesn't own. + +## What DtBox Does NOT Do + +- **Layout**: No `direction`, `gap`, `justify`, or `align`. Use [DtStack](/components/stack.md). +- **Typography**: No `font`, `size`, `tone`, or `weight`. Use [DtText](/components/text.md). +- **Flex child**: No `flexGrow`, `flexShrink`, or `alignSelf`. Use CSS Utility classes. +- **Positioning**: No `position`, `inset`, or `zIndex`. Use CSS Utility classes. + +These boundaries are intentional as DtBox is a surface primitive, not a general-purpose styled element. The `class` attribute remains the escape hatch for anything outside its prop surface. + +See the [full component documentation](/components/box.md) for all prop values, demos, and usage guidance. + +
+ + diff --git a/apps/dialtone-documentation/docs/foundations/typography/index.md b/apps/dialtone-documentation/docs/foundations/typography/index.md index 1c4d70f84c..5c051a43e0 100644 --- a/apps/dialtone-documentation/docs/foundations/typography/index.md +++ b/apps/dialtone-documentation/docs/foundations/typography/index.md @@ -237,7 +237,7 @@ vueCode=' Katie Rodriguez
- + Available diff --git a/apps/dialtone-documentation/docs/scratch.md b/apps/dialtone-documentation/docs/scratch.md index 0da2be8c44..040c872093 100644 --- a/apps/dialtone-documentation/docs/scratch.md +++ b/apps/dialtone-documentation/docs/scratch.md @@ -451,7 +451,6 @@ Real-world patterns showing how `v-dt-focusgroup` composes with Dialtone compone ### Contact List, with custom selector - ```vue demo @@ -1187,7 +1186,7 @@ Real-world patterns showing how `v-dt-focusgroup` composes with Dialtone compone
Default - + Action completed successfully. @@ -1205,7 +1204,7 @@ Real-world patterns showing how `v-dt-focusgroup` composes with Dialtone compone Visually prominent variant with filled background. - + Visually prominent variant with filled background. @@ -1235,7 +1234,7 @@ Real-world patterns showing how `v-dt-focusgroup` composes with Dialtone compone Banners are more prominent than notices. - + Action completed successfully. @@ -1250,7 +1249,7 @@ Real-world patterns showing how `v-dt-focusgroup` composes with Dialtone compone Banners are more prominent than notices. - + Action completed successfully. @@ -1269,7 +1268,7 @@ Real-world patterns showing how `v-dt-focusgroup` composes with Dialtone compone Default - + @@ -1277,7 +1276,7 @@ Real-world patterns showing how `v-dt-focusgroup` composes with Dialtone compone Important - + @@ -1344,237 +1343,318 @@ Real-world patterns showing how `v-dt-focusgroup` composes with Dialtone compone + + + + + + + + DtBox V1 + + + - Traveling Indicator Stress Test - - 1. All four variants - - - Default (::after underline) - - - - Outlined - - - - Muted + Outlined - - - - Muted Active (background) - - - - - 2. All sizes (width variance stress) - - - Size 100 (xs) - - - - Size 200 (sm) - - - - Size 300 (md) — default - - - - Size 400 (lg) - - - - Size 500 (xl) - - - - - 3. Spread modes (indicator width morphing) - - - spread="none" (default) - - - - spread="grow" - - - - spread="equal" - - - - - 4. Vertical orientation - - - Default vertical - - - - Outlined vertical - - - - Muted vertical - - - - - 5. Borderless - - - Borderless default - - - - Borderless outlined - - - - - 6. Auto activation mode (arrow keys should NOT animate) - - - Auto mode — click should animate, arrows should snap - - - - Auto mode + outlined - - - - - 7. Disabled (should do nothing) - - Whole group disabled - - - - 8. showIndicatorTransition=false (animation suppressed) - - Should switch instantly, no slide - - - Panel 1 - Panel 2 - Panel 3 - - - - 9. Multiple instances on same page (no conflicts) - Click tabs in one group while another is mid-animation. They should not interfere. - - - Group A - - - - Group B - - - - Group C - - - - - 10. Extreme tab width differences (scale morphing stress) - - Short vs very long labels — watch the scale animation - - - Panel A - Panel Long - Panel B - - - - Panel A - Panel Long - Panel B - - - - 11. Many tabs (potential wrapping) - - Does the animation break when tabs wrap to a second row? - - - Panel Alpha - Panel Bravo - Panel Charlie - Panel Delta - Panel Echo - Panel Foxtrot - Panel Golf - Panel Hotel - Panel India - Panel Juliet - - - - 12. RTL direction - - Does the indicator slide the correct direction in RTL? -
- -
-
- - 13. Inverted - - - - - 14. Rapid click test - - Click tabs as fast as possible — animation should cancel cleanly, no stuck states - - - - 15. Combined extremes - - - Outlined + spread=equal + size 500 - - - - Muted + vertical + borderless - - - - Muted + outlined + spread=grow + size 100 - - + + Padding + Surface Combos + + +```vue demo + + Box demo + Box demo + Box demo + Box demo + Box demo + +``` + + + + + + + as Prop Variants + + + DtBox renders as different HTML elements via the as prop. + Inspect elements to verify the rendered tag. + + +```vue demo + + Box demo + Box demo + Box demo + Box demo + Box demo + +``` + + + + + + + Padding Cascade + + + Specific axes override shorthand: paddingInline overrides padding for left/right, + paddingBlockStart overrides paddingBlock for top. + + +```vue demo + + Box demo + Box demo + Box demo + Box demo + Box demo + +``` + + + + + + + Nested Inheritance Isolation + + + @property registrations prevent custom property inheritance. + Inner boxes should NOT inherit outer padding or surface. + + +```vue demo + + +
Outer box
+ Inner box (should not inherit outer) + Inner box, no props (should have 0 padding, transparent surface) +
+
+``` + +
+ + + + + Surface Opaque Variants + + + Opaque surfaces use solid colors instead of alpha transparency, + preventing bleed-through on layered backgrounds. + + +```vue demo + + +
Parent surface="brand"
+ + Box demo + Box demo + Box demo + Box demo
+
+``` + +
+ + + + + Utility Class Escape Hatch + + + DtBox accepts standard class attributes for one-off styling that falls + outside its prop API. Utility classes compose naturally with the component. + + +```vue demo + + Box demo + +``` + +```vue demo +
+ + Box demo + + + Scroll content below the sticky box... +
+ ...end of scroll content. +
+
+``` + +
+
+ + + + + + DtBox + + Basic padding + surface + +```vue demo + + Box demo + Box demo + Box demo + +``` + + Padding cascade + +```vue demo + + Box demo + Box demo + Box demo + +``` + + Polymorphic as + +```vue demo + + Box demo + Box demo + Box demo + +``` + + Nested inheritance isolation + +```vue demo + + +
Outer box
+ Inner box (independent) + Inner box, no padding (should be 0) +
+
+``` + + Utility class escape hatch + +```vue demo + + Box demo + +``` + + Card compositions + +```vue demo + + Box demo + Box demo + Box demo + +``` + + Shadow scale + +```vue demo + + Box demo + Box demo + Box demo + Box demo + Box demo + +``` + + Border radius variants + +```vue demo + + 0 long label lorem
second line
+ 200 long label lorem
second line
+ 400 long label lorem
second line
+ 600 long label lorem
second line
+ pill long label lorem + circle +
+``` + + No border props = invisible border + +```vue demo + + Box demo (no border props) + Box demo (with border props) + +``` + + Layout token sizing + +```vue demo + + Box demo + Box demo + +``` + + Class escape hatch for arbitrary sizing + +```vue demo + + Box demo + Box demo + +``` + + Overflow + borderRadius clipping + +```vue demo + +
+ Tall content clipped by overflow="hidden" and borderRadius="400" +
+
+``` + + Scrollbar integration + +```vue demo + + +
Scrollable item {{ i }}
+
+
+``` + +```vue demo + + +
Scrollable item {{ i }}
+
+
+``` + +```vue demo + + +
Scrollable item {{ i }} (native scrollbar)
+
+
+``` +
- - - + +
+ +
diff --git a/apps/dialtone-documentation/docs/utilities/typography/font-color.md b/apps/dialtone-documentation/docs/utilities/typography/font-color.md index 0e64f1dba4..af75534fc1 100644 --- a/apps/dialtone-documentation/docs/utilities/typography/font-color.md +++ b/apps/dialtone-documentation/docs/utilities/typography/font-color.md @@ -20,8 +20,8 @@ Use [DtText's](/components/text.html#tone) `tone` prop to declare the text's ton muted disabled placeholder - success - success-strong + positive + positive-strong warning critical critical-strong @@ -46,7 +46,7 @@ Use `d-fc-{color}` to change an element's text color. Use `h:d-fc-{color}` to change an element's text color `:hover` state. ```vue demo -Hover over me +Hover over me ``` ## Focus @@ -54,7 +54,7 @@ Use `h:d-fc-{color}` to change an element's text color `:hover` state. Use `f:d-fc-{color}` to change an element's text color `:focus` and `:focus-within` state. ```vue demo -Focus me +Focus me ``` ## Focus Visible @@ -62,7 +62,7 @@ Use `f:d-fc-{color}` to change an element's text color `:focus` and `:focus-with Use `fv:d-fc-{color}` to change an element's text color on `:focus-visible` state [only when focused by keyboard]. ```vue demo -Keyboard focus me +Keyboard focus me ``` ## Inverted @@ -79,8 +79,8 @@ Use `fv:d-fc-{color}` to change an element's text color on `:focus-visible` stat muted disabled placeholder - success - success-strong + positive + positive-strong warning critical critical-strong @@ -92,8 +92,8 @@ Use `fv:d-fc-{color}` to change an element's text color on `:focus-visible` stat muted disabled placeholder - success - success-strong + positive + positive-strong warning critical critical-strong @@ -110,8 +110,8 @@ vueCode=' muted disabled placeholder - success - success-strong + positive + positive-strong warning critical critical-strong diff --git a/apps/dialtone-documentation/docs/utilities/typography/text-opacity.md b/apps/dialtone-documentation/docs/utilities/typography/text-opacity.md index cbc8a87a18..ae1e4142f7 100644 --- a/apps/dialtone-documentation/docs/utilities/typography/text-opacity.md +++ b/apps/dialtone-documentation/docs/utilities/typography/text-opacity.md @@ -42,7 +42,7 @@ Text opacity CSS Utilities won't be inherited by its children. A font-color util
- + Apply each utility class to the same element diff --git a/common/components_list.js b/common/components_list.js index c5d0b04fcb..41727a17de 100644 --- a/common/components_list.js +++ b/common/components_list.js @@ -3,6 +3,7 @@ module.exports = [ 'avatar.vue', 'badge.vue', 'banner.vue', + 'box.vue', 'breadcrumbs.vue', 'button.vue', 'button_group.vue', diff --git a/packages/combinator/src/components/code_editor/code_editor.vue b/packages/combinator/src/components/code_editor/code_editor.vue index fafeee5b8f..6efd185be2 100644 --- a/packages/combinator/src/components/code_editor/code_editor.vue +++ b/packages/combinator/src/components/code_editor/code_editor.vue @@ -59,7 +59,7 @@ @@ -77,7 +77,7 @@ diff --git a/packages/combinator/src/components/option_bar/option_bar_member_group.vue b/packages/combinator/src/components/option_bar/option_bar_member_group.vue index 32cae3fc08..17bfe9d181 100644 --- a/packages/combinator/src/components/option_bar/option_bar_member_group.vue +++ b/packages/combinator/src/components/option_bar/option_bar_member_group.vue @@ -51,6 +51,14 @@ const PROP_PRIORITY = [ 'title', 'as', 'label', 'importance', 'kind', 'size', 'placement', 'tone', 'align', 'density', 'strength', 'type', 'underline', 'selected', 'active', 'disabled', 'showDivider', 'color', 'description', + 'scrollbar', 'surface', + 'borderColor', 'borderRadius', + 'borderWidth', 'borderWidthBlock', 'borderWidthBlockEnd', 'borderWidthBlockStart', + 'borderWidthInline', 'borderWidthInlineEnd', 'borderWidthInlineStart', + 'padding', 'paddingBlock', 'paddingBlockEnd', 'paddingBlockStart', + 'paddingInline', 'paddingInlineEnd', 'paddingInlineStart', + 'blockSize', 'inlineSize', 'maxBlockSize', 'minBlockSize', 'maxInlineSize', 'minInlineSize', + 'shadow', 'overflow', ]; const SLOT_PRIORITY = ['start', 'end', 'inlineStart', 'inlineEnd', 'blockStart', 'blockEnd', 'leading', 'trailing']; diff --git a/packages/combinator/src/lib/tokens.js b/packages/combinator/src/lib/tokens.js index dbcf45265d..b2108972b5 100644 --- a/packages/combinator/src/lib/tokens.js +++ b/packages/combinator/src/lib/tokens.js @@ -95,6 +95,15 @@ export function resolveTokenValue (category, value, propValues) { case 'color': if (categoryArgs[0]) result = resolveColor(categoryArgs[0], categoryArgs[1] || 'color', value); break; + case 'border-width': + result = resolveBorderWidth(value); + break; + case 'border-radius': + result = resolveBorderRadius(value); + break; + case 'layout': + result = resolveLayout(value); + break; } cache.set(cacheKey, result); @@ -172,6 +181,31 @@ function resolveIconSize (value) { return px ? formatPx(px) : null; } +function resolveBorderWidth (value) { + const px = resolveCssVar(`--dt-size-border-${value}`); + return px ? formatPx(px) : null; +} + +function resolveBorderRadius (value) { + const px = resolveCssVar(`--dt-size-radius-${value}`); + if (!px) return null; + // 'pill' and 'circle' resolve to very large / percentage + // values — show the token name instead of a meaningless px. + if (value === 'pill' || value === 'circle') return value; + return formatPx(px); +} + +function resolveLayout (value) { + // Percent values ('50-percent') resolve to raw strings like + // '50%' — read directly,don't run through width measurement. + if (String(value).endsWith('-percent')) { + const raw = getComputedStyle(document.documentElement).getPropertyValue(`--dt-layout-${value}`).trim(); + return raw || null; + } + const px = resolveCssVar(`--dt-layout-${value}`); + return px ? formatPx(px) : null; +} + function resolveComponentSize (componentClass, value) { const el = getMeasureElement(); try { diff --git a/packages/combinator/src/variants/variants.js b/packages/combinator/src/variants/variants.js index 63e3427dcc..1ae54034d1 100644 --- a/packages/combinator/src/variants/variants.js +++ b/packages/combinator/src/variants/variants.js @@ -3,6 +3,7 @@ import DtBadge from './variants_badge.js'; import DtBanner from './variants_banner.js'; import DtButton from './variants_button.js'; import DtButtonGroup from './variants_button_group.js'; +import DtBox from './variants_box.js'; import DtBreadcrumbs from './variants_breadcrumbs.js'; import DtCard from './variants_card.js'; import DtCheckbox from './variants_checkbox.js'; @@ -63,6 +64,7 @@ export default function variants () { DtBanner, DtButton, DtButtonGroup, + DtBox, DtBreadcrumbs, DtCard, DtCheckbox, diff --git a/packages/combinator/src/variants/variants_box.js b/packages/combinator/src/variants/variants_box.js new file mode 100644 index 0000000000..5999632e07 --- /dev/null +++ b/packages/combinator/src/variants/variants_box.js @@ -0,0 +1,168 @@ +/* eslint-disable max-len */ +export default { + defaults: { + props: { + surface: { tokenCategory: 'color:d-box--surface-:--box-surface' }, + borderColor: { tokenCategory: 'color:d-box--bc-:--box-bc' }, + padding: { tokenCategory: 'spacing' }, + paddingInline: { tokenCategory: 'spacing' }, + paddingInlineStart: { tokenCategory: 'spacing' }, + paddingInlineEnd: { tokenCategory: 'spacing' }, + paddingBlock: { tokenCategory: 'spacing' }, + paddingBlockStart: { tokenCategory: 'spacing' }, + paddingBlockEnd: { tokenCategory: 'spacing' }, + borderWidth: { tokenCategory: 'border-width' }, + borderWidthInline: { tokenCategory: 'border-width' }, + borderWidthInlineStart: { tokenCategory: 'border-width' }, + borderWidthInlineEnd: { tokenCategory: 'border-width' }, + borderWidthBlock: { tokenCategory: 'border-width' }, + borderWidthBlockStart: { tokenCategory: 'border-width' }, + borderWidthBlockEnd: { tokenCategory: 'border-width' }, + borderRadius: { tokenCategory: 'border-radius' }, + inlineSize: { tokenCategory: 'layout' }, + blockSize: { tokenCategory: 'layout' }, + minInlineSize: { tokenCategory: 'layout' }, + maxInlineSize: { tokenCategory: 'layout' }, + minBlockSize: { tokenCategory: 'layout' }, + maxBlockSize: { tokenCategory: 'layout' }, + }, + }, + + default: { + slots: { + default: { initialValue: 'Box content' }, + }, + }, + + + 'custom card': { + props: { + as: { initialValue: 'aside' }, + padding: { initialValue: '200' }, + surface: { initialValue: 'primary' }, + borderWidth: { initialValue: '100' }, + borderColor: { initialValue: 'subtle' }, + borderRadius: { initialValue: '400' }, + shadow: { initialValue: 'card' }, + }, + slots: { + default: { initialValue: ` + Card title + Card body content with some descriptive text. +` }, + }, + }, + + 'critical surface': { + props: { + padding: { initialValue: '300' }, + surface: { initialValue: 'critical' }, + borderColor: { initialValue: 'critical' }, + borderWidth: { initialValue: '100' }, + borderRadius: { initialValue: '500' }, + }, + slots: { + default: { initialValue: ` + Critical container +` }, + }, + }, + + 'combined with DtStack and DtText': { + props: { + padding: { initialValue: '200' }, + surface: { initialValue: 'moderate-opaque' }, + borderRadius: { initialValue: '450' }, + shadow: { initialValue: 'card' }, + }, + slots: { + default: { initialValue: ` + + First + + + Second + + + Third + +` }, + }, + }, + + 'sized': { + props: { + padding: { initialValue: '200' }, + surface: { initialValue: 'moderate' }, + borderWidth: { initialValue: '100' }, + inlineSize: { initialValue: '500' }, + blockSize: { initialValue: '600' }, + }, + slots: { + default: { initialValue: 'Fixed size' }, + }, + }, + + 'shadow': { + props: { + padding: { initialValue: '200' }, + surface: { initialValue: 'primary' }, + borderRadius: { initialValue: '200' }, + shadow: { initialValue: 'large' }, + }, + slots: { + default: { initialValue: 'Large shadow' }, + }, + }, + + 'pill radius': { + props: { + padding: { initialValue: '100' }, + paddingInline: { initialValue: '200' }, + surface: { initialValue: 'moderate' }, + borderRadius: { initialValue: 'pill' }, + }, + slots: { + default: { initialValue: ` + Pill shape +` }, + }, + }, + + 'composed layout': { + props: { + padding: { initialValue: '200' }, + surface: { initialValue: 'primary' }, + borderWidth: { initialValue: '100' }, + borderColor: { initialValue: 'subtle' }, + borderRadius: { initialValue: '400' }, + }, + slots: { + default: { initialValue: ` + + Title + + + Add Box + + + + + Box 1 + + + Box 2 + + + Box 3 + + + Box 4 + + +` }, + }, + }, +}; diff --git a/packages/dialtone-css/lib/build/less/components/box.less b/packages/dialtone-css/lib/build/less/components/box.less new file mode 100644 index 0000000000..ae97ff36e3 --- /dev/null +++ b/packages/dialtone-css/lib/build/less/components/box.less @@ -0,0 +1,241 @@ +// +// DIALTONE +// COMPONENTS: BOX +// +// These are the styles for box component. +// +// +// TABLE OF CONTENTS +// • @PROPERTY REGISTRATIONS +// • PARAMETRIC MIXINS +// • VALUE LISTS +// • BASE STYLE +// • MODIFIERS + +// @@ @PROPERTY REGISTRATIONS +// ---------------------------------------------------------------------------- +@property --box-p { syntax: "*"; inherits: false; } + +@property --box-pi { syntax: "*"; inherits: false; } + +@property --box-pis { syntax: "*"; inherits: false; } + +@property --box-pie { syntax: "*"; inherits: false; } + +@property --box-pbl { syntax: "*"; inherits: false; } + +@property --box-pbs { syntax: "*"; inherits: false; } + +@property --box-pbe { syntax: "*"; inherits: false; } + +@property --box-surface { syntax: ""; inherits: false; initial-value: transparent; } + +@property --box-bc { syntax: ""; inherits: false; initial-value: transparent; } + +@property --box-bw { syntax: "*"; inherits: false; } + +@property --box-bwi { syntax: "*"; inherits: false; } + +@property --box-bwis { syntax: "*"; inherits: false; } + +@property --box-bwie { syntax: "*"; inherits: false; } + +@property --box-bwbl { syntax: "*"; inherits: false; } + +@property --box-bwbs { syntax: "*"; inherits: false; } + +@property --box-bwbe { syntax: "*"; inherits: false; } + +@property --box-br { syntax: ""; inherits: false; initial-value: 0; } + +@property --box-shadow { syntax: "*"; inherits: false; } + +@property --box-of { syntax: "*"; inherits: false; } + +// ============================================================================ +// $ PARAMETRIC MIXINS +// ---------------------------------------------------------------------------- +._box-token(@prefix; @prop; @token; @names) { + each(@names, { + .d-box--@{prefix}-@{value} { @{prop}: ~"var(--@{token}-@{value})"; } + }); +} + +._box-layout(@prop) { + &-25 { @{prop}: var(--dt-layout-25); } + &-50 { @{prop}: var(--dt-layout-50); } + &-75 { @{prop}: var(--dt-layout-75); } + each(range(0, 1600, 100), { + &-@{value} { @{prop}: ~"var(--dt-layout-@{value})"; } + }); + // Percentage tokens + each(@box-layout-percent-values, { + &-@{value}-percent { @{prop}: ~"var(--dt-layout-@{value}-percent)"; } + }); +} + +._box-border-width(@prop) { + each(@box-border-width-values, { + &-@{value} { @{prop}: ~"var(--dt-size-border-@{value})"; } + }); +} + +._box-spacing(@prop) { + // Stops outside both range() sequences: 1 (not a multiple of 25), 525 (not a multiple of 50) + &-1 { @{prop}: var(--dt-spacing-1); } + &-525 { @{prop}: var(--dt-spacing-525); } + + each(range(0, 200, 25), { + &-@{value} { @{prop}: ~"var(--dt-spacing-@{value})"; } + }); + + each(range(250, 800, 50), { + &-@{value} { @{prop}: ~"var(--dt-spacing-@{value})"; } + }); +} + +// ============================================================================ +// $ VALUE LISTS +// ---------------------------------------------------------------------------- +// `positive*` is intentionally absent from these lists. Surface and border +// tokens still use legacy `success*` naming (no `--dt-color-surface-positive-*` +// exists yet), so `positive*` modifier classes are generated from explicit +// alias blocks below that map to `--dt-color-surface-success-*` / +// `--dt-color-border-success-*`. Once DLT-3331 lands the token rename, +// `positive*` moves into these lists and the alias blocks can be dropped. +@box-surface-values: primary, secondary, moderate, bold, strong, contrast, backdrop, brand, info, warning, critical, brand-subtle, brand-strong, info-subtle, info-strong, warning-subtle, warning-strong, critical-subtle, critical-strong, primary-opaque, secondary-opaque, moderate-opaque, bold-opaque, strong-opaque, contrast-opaque, brand-opaque, brand-subtle-opaque, info-opaque, info-subtle-opaque, warning-opaque, warning-subtle-opaque, critical-opaque, critical-subtle-opaque; + +@box-border-color-values: subtle, default, moderate, bold, accent, focus, brand, warning, critical, brand-subtle, brand-strong, warning-subtle, warning-strong, critical-subtle, critical-strong; + +@box-border-width-values: 0, 50, 100, 150, 200, 300, 400; +@box-layout-percent-values: 10, 20, 25, 30, 33, 40, 50, 60, 66, 70, 75, 80, 90, 95, 100; +@box-shadow-values: small, medium, large, extra-large, card; +@box-overflow-values: hidden, scroll, auto, clip, visible; + +@layer dialtone.components { + +// @@ BASE STYLE +// ---------------------------------------------------------------------------- +.d-box { + box-sizing: border-box; + + // Overflow + overflow: var(--box-of, visible); + + // Surface — OKLCH relative syntax supports d-bgo* opacity utilities + background-color: oklch(from var(--box-surface) l c h / var(--bgo, alpha)); + border-color: oklch(from var(--box-bc) l c h / var(--bco, alpha)); + + // Border — always solid, no visible border until borderWidth prop is set. + // Width cascade: specific → axis → shorthand → 0 + border-style: solid; + border-block-start-width: var(--box-bwbs, var(--box-bwbl, var(--box-bw, 0))); + border-block-end-width: var(--box-bwbe, var(--box-bwbl, var(--box-bw, 0))); + border-inline-start-width: var(--box-bwis, var(--box-bwi, var(--box-bw, 0))); + border-inline-end-width: var(--box-bwie, var(--box-bwi, var(--box-bw, 0))); + border-radius: var(--box-br); + + // Shadow + box-shadow: var(--box-shadow, none); + + // Padding cascade: specific → axis → shorthand → 0 + padding-block: var(--box-pbs, var(--box-pbl, var(--box-p, 0))) var(--box-pbe, var(--box-pbl, var(--box-p, 0))); + padding-inline: var(--box-pis, var(--box-pi, var(--box-p, 0))) var(--box-pie, var(--box-pi, var(--box-p, 0))); +} + +// ============================================================================ +// $ MODIFIERS +// ---------------------------------------------------------------------------- +// $$ PADDING +// ---------------------------------------------------------------------------- +.d-box--p { ._box-spacing(--box-p); } +.d-box--pi { ._box-spacing(--box-pi); } +.d-box--pis { ._box-spacing(--box-pis); } +.d-box--pie { ._box-spacing(--box-pie); } +.d-box--pbl { ._box-spacing(--box-pbl); } +.d-box--pbs { ._box-spacing(--box-pbs); } +.d-box--pbe { ._box-spacing(--box-pbe); } + +// ---------------------------------------------------------------------------- +// $$ SURFACE +// ---------------------------------------------------------------------------- +._box-token(surface; --box-surface; dt-color-surface; @box-surface-values); + +// `positive` prop → maps to `--dt-color-surface-success-*` tokens (surface tokens use legacy `success` naming; see DLT-3331) +.d-box--surface-positive { --box-surface: var(--dt-color-surface-success); } +.d-box--surface-positive-subtle { --box-surface: var(--dt-color-surface-success-subtle); } +.d-box--surface-positive-strong { --box-surface: var(--dt-color-surface-success-strong); } +.d-box--surface-positive-opaque { --box-surface: var(--dt-color-surface-success-opaque); } +.d-box--surface-positive-subtle-opaque { --box-surface: var(--dt-color-surface-success-subtle-opaque); } + +// ---------------------------------------------------------------------------- +// $$ BORDER +// ---------------------------------------------------------------------------- +// ── Border ────────────────────────────────────────────────────────────────── +.d-box--bc-transparent { --box-bc: transparent; } +._box-token(bc; --box-bc; dt-color-border; @box-border-color-values); + +// `positive` prop → maps to `--dt-color-border-success-*` tokens (border tokens use legacy `success` naming; see DLT-3331) +.d-box--bc-positive { --box-bc: var(--dt-color-border-success); } +.d-box--bc-positive-subtle { --box-bc: var(--dt-color-border-success-subtle); } +.d-box--bc-positive-strong { --box-bc: var(--dt-color-border-success-strong); } + +@box-border-radius-values: 0, 100, 200, 300, 350, 400, 450, 500, 600; + +// Border width — 7 axes × border-width scale +.d-box--bw { ._box-border-width(--box-bw); } +.d-box--bwi { ._box-border-width(--box-bwi); } +.d-box--bwis { ._box-border-width(--box-bwis); } +.d-box--bwie { ._box-border-width(--box-bwie); } +.d-box--bwbl { ._box-border-width(--box-bwbl); } +.d-box--bwbs { ._box-border-width(--box-bwbs); } +.d-box--bwbe { ._box-border-width(--box-bwbe); } + +// Border radius +each(@box-border-radius-values, { + .d-box--br-@{value} { --box-br: ~"var(--dt-size-radius-@{value})"; } +}); +.d-box--br-pill { --box-br: var(--dt-size-radius-pill); } +.d-box--br-circle { --box-br: var(--dt-size-radius-circle); } + +// ---------------------------------------------------------------------------- +// $$ SHADOW +// ---------------------------------------------------------------------------- +// ── Shadow ────────────────────────────────────────────────────────────────── +._box-token(shadow; --box-shadow; dt-shadow; @box-shadow-values); + +// ---------------------------------------------------------------------------- +// $$ SIZING +// ---------------------------------------------------------------------------- +// Layout token values → modifier classes. Raw CSS fallback → inline :style. +.d-box--is { ._box-layout(inline-size); } +.d-box--bls { ._box-layout(block-size); } +.d-box--min-is { ._box-layout(min-inline-size); } +.d-box--max-is { ._box-layout(max-inline-size); } +.d-box--min-bls { ._box-layout(min-block-size); } +.d-box--max-bls { ._box-layout(max-block-size); } + +// ---------------------------------------------------------------------------- +// $$ OVERFLOW +// ---------------------------------------------------------------------------- +each(@box-overflow-values, { + .d-box--of-@{value} { --box-of: @value; } +}); + +// ---------------------------------------------------------------------------- +// $$ SCROLLBAR +// ---------------------------------------------------------------------------- +// Inner viewport wrapper — only rendered when scrollbar prop is active. +// OverlayScrollbars uses this as the viewport element (el.children[0]). +// :has() detects the wrapper — CSS reacts to its presence without JS class toggling. +.d-box__scrollbar-content { + inline-size: 100%; + block-size: 100%; + overflow: auto; // Native scroll fallback when OverlayScrollbars is unavailable +} + +.d-box:has(> .d-box__scrollbar-content) { + overflow: hidden; +} + +} diff --git a/packages/dialtone-css/lib/build/less/components/text.less b/packages/dialtone-css/lib/build/less/components/text.less index 6025281dfa..fa9f1922a7 100644 --- a/packages/dialtone-css/lib/build/less/components/text.less +++ b/packages/dialtone-css/lib/build/less/components/text.less @@ -231,8 +231,6 @@ &--tone-critical-strong { --text-tone: var(--dt-color-foreground-critical-strong); } &--tone-positive { --text-tone: var(--dt-color-foreground-positive); } &--tone-positive-strong { --text-tone: var(--dt-color-foreground-positive-strong); } - &--tone-success { --text-tone: var(--dt-color-foreground-success); } - &--tone-success-strong { --text-tone: var(--dt-color-foreground-success-strong); } &--tone-warning { --text-tone: var(--dt-color-foreground-warning); } &--tone-neutral-black { --text-tone: var(--dt-color-neutral-black); } &--tone-neutral-white { --text-tone: var(--dt-color-neutral-white); } diff --git a/packages/dialtone-css/lib/build/less/dialtone.less b/packages/dialtone-css/lib/build/less/dialtone.less index 0a3bacd7c6..73635e240b 100644 --- a/packages/dialtone-css/lib/build/less/dialtone.less +++ b/packages/dialtone-css/lib/build/less/dialtone.less @@ -15,6 +15,7 @@ // -- COMPONENTS @import 'components/root-layout'; +@import 'components/box'; @import 'components/stack'; @import 'components/text'; @import 'components/link'; diff --git a/packages/dialtone-css/lib/build/less/recipes/leftbar_row.less b/packages/dialtone-css/lib/build/less/recipes/leftbar_row.less index b75393a149..0553fc188a 100644 --- a/packages/dialtone-css/lib/build/less/recipes/leftbar_row.less +++ b/packages/dialtone-css/lib/build/less/recipes/leftbar_row.less @@ -204,7 +204,8 @@ --button-color-background: color-mix(in srgb, var(--dt-shell-mention-color-surface-primary) 30%, var(--dt-color-surface-primary) 100%); } - .d-fc-success { + .d-fc-success, + .d-fc-positive { color:var(--dt-shell-color-foreground-positive)!important; } diff --git a/packages/dialtone-docs/src/content/workflows/workflow-component-lifecycle.md b/packages/dialtone-docs/src/content/workflows/workflow-component-lifecycle.md index 0ba29589df..5951bc3b64 100644 --- a/packages/dialtone-docs/src/content/workflows/workflow-component-lifecycle.md +++ b/packages/dialtone-docs/src/content/workflows/workflow-component-lifecycle.md @@ -53,7 +53,7 @@ The table is rendered by `ComponentHealthStatusTable.vue` using color-coded icon | Icon | Color | Meaning | |------|-------|---------| -| Check circle | Green (`d-fc-success`) | Ready | +| Check circle | Green (`d-fc-positive`) | Ready | | Tools | Orange (`d-fc-warning`) | In progress / beta | | Box select | Red (`d-fc-critical`) | Planned | | Box | Gray (`d-fc-muted`) | N/A | diff --git a/packages/dialtone-vue/components/box/box.stories.js b/packages/dialtone-vue/components/box/box.stories.js new file mode 100644 index 0000000000..c6713e1102 --- /dev/null +++ b/packages/dialtone-vue/components/box/box.stories.js @@ -0,0 +1,179 @@ +import DtBox from './box.vue'; +import DtStack from '@/components/stack/stack.vue'; +import DtText from '@/components/text/text.vue'; +import BoxVariants from './box_variants.story.vue'; +import { createTemplateFromVueFile } from '@/common/storybook_utils'; +import { + DT_BOX_AS_VALUES, + DT_BOX_SPACING_VALUES, + DT_BOX_SURFACE_VALUES, + DT_BOX_BORDER_COLOR_VALUES, + DT_BOX_BORDER_WIDTH_VALUES, + DT_BOX_BORDER_RADIUS_VALUES, + DT_BOX_SHADOW_VALUES, +} from './box_constants.js'; + +export const argsData = { + as: 'div', + padding: '200', + surface: 'secondary', + borderColor: undefined, + borderWidth: undefined, + borderRadius: undefined, + shadow: undefined, +}; + +export const argTypesData = { + default: { + control: { type: null }, + description: 'Slot for main content', + }, + as: { + control: 'select', + options: DT_BOX_AS_VALUES, + }, + padding: { + control: 'select', + options: [undefined, ...DT_BOX_SPACING_VALUES], + }, + paddingInline: { + control: 'select', + options: [undefined, ...DT_BOX_SPACING_VALUES], + }, + paddingInlineStart: { + control: 'select', + options: [undefined, ...DT_BOX_SPACING_VALUES], + }, + paddingInlineEnd: { + control: 'select', + options: [undefined, ...DT_BOX_SPACING_VALUES], + }, + paddingBlock: { + control: 'select', + options: [undefined, ...DT_BOX_SPACING_VALUES], + }, + paddingBlockStart: { + control: 'select', + options: [undefined, ...DT_BOX_SPACING_VALUES], + }, + paddingBlockEnd: { + control: 'select', + options: [undefined, ...DT_BOX_SPACING_VALUES], + }, + surface: { + control: 'select', + options: [undefined, ...DT_BOX_SURFACE_VALUES], + }, + borderColor: { + control: 'select', + options: [undefined, ...DT_BOX_BORDER_COLOR_VALUES], + }, + borderWidth: { + control: 'select', + options: [undefined, ...DT_BOX_BORDER_WIDTH_VALUES], + }, + borderRadius: { + control: 'select', + options: [undefined, ...DT_BOX_BORDER_RADIUS_VALUES], + }, + shadow: { + control: 'select', + options: [undefined, ...DT_BOX_SHADOW_VALUES], + }, +}; + +export default { + title: 'Components/Box', + component: DtBox, + args: argsData, + argTypes: argTypesData, + excludeStories: /.*Data$/, +}; + +export const Default = { + render: (args) => ({ + components: { DtBox }, + setup () { return { args }; }, + template: 'Box content', + }), + args: {}, +}; + +const VariantsTemplate = (args, { argTypes }) => + createTemplateFromVueFile(args, argTypes, BoxVariants); + +export const Variants = { + render: VariantsTemplate, + args: {}, + parameters: { controls: { disable: true } }, +}; + +export const PaddingCascade = { + render: () => ({ + components: { DtBox, DtStack, DtText }, + template: ` + + + padding="300" (all sides) + + + + paddingInline="100" + + + + paddingInlineStart="0" + + + + paddingBlock="200" + paddingBlockEnd="500" + + + `, + }), + parameters: { controls: { disable: true } }, +}; + +export const CardComposition = { + render: () => ({ + components: { DtBox, DtStack, DtText }, + template: ` + + + Card: subtle border + radius + card shadow + + + Elevated card + + + Brand card: no shadow + + + `, + }), + parameters: { controls: { disable: true } }, +}; + +export const NestedInheritanceIsolation = { + render: () => ({ + components: { DtBox, DtStack, DtText }, + template: ` + + + + Outer: padding 500 + + Inner: padding 100 (no inheritance leak) + + + + + + Outer: padding 500 + + Inner: no padding (should be 0, not 500) + + + + + `, + }), + parameters: { controls: { disable: true } }, +}; diff --git a/packages/dialtone-vue/components/box/box.test.js b/packages/dialtone-vue/components/box/box.test.js new file mode 100644 index 0000000000..0f3599ae51 --- /dev/null +++ b/packages/dialtone-vue/components/box/box.test.js @@ -0,0 +1,400 @@ +import { mount } from '@vue/test-utils'; +import DtBox from './box.vue'; +import { + DT_BOX_AS_VALUES, + DT_BOX_SURFACE_VALUES, + DT_BOX_BORDER_COLOR_VALUES, + DT_BOX_BORDER_WIDTH_VALUES, + DT_BOX_BORDER_RADIUS_VALUES, + DT_BOX_SHADOW_VALUES, + DT_BOX_OVERFLOW_VALUES, +} from './box_constants.js'; + +describe('DtBox', () => { + const slotContent = 'Box content'; + let wrapper; + + const mountComponent = (props = {}, attrs = {}, slots = {}) => { + wrapper = mount(DtBox, { + props, + attrs, + slots: { + default: slotContent, + ...slots, + }, + }); + return wrapper; + }; + + afterEach(() => { + wrapper?.unmount(); + }); + + // ── Presentation ────────────────────────────────────────── + + it('renders with d-box base class', () => { + const wrapper = mountComponent(); + + expect(wrapper.classes()).toContain('d-box'); + }); + + it('renders slot content', () => { + const wrapper = mountComponent(); + + expect(wrapper.text()).toBe(slotContent); + }); + + it('renders as div by default', () => { + const wrapper = mountComponent(); + + expect(wrapper.element.tagName).toBe('DIV'); + }); + + it.each( + DT_BOX_AS_VALUES.filter(tag => tag !== 'div'), + )('renders as %s when as="%s"', (tag) => { + const wrapper = mountComponent({ as: tag }); + + expect(wrapper.element.tagName).toBe(tag.toUpperCase()); + }); + + it('applies data-qa attribute', () => { + const wrapper = mountComponent(); + + expect(wrapper.attributes('data-qa')).toBe('dt-box'); + }); + + // ── Padding ─────────────────────────────────────────────── + + it('applies padding modifier class', () => { + const wrapper = mountComponent({ padding: '200' }); + + expect(wrapper.classes()).toContain('d-box--p-200'); + }); + + it('applies paddingInline modifier class', () => { + const wrapper = mountComponent({ paddingInline: '100' }); + + expect(wrapper.classes()).toContain('d-box--pi-100'); + }); + + it('applies paddingInlineStart modifier class', () => { + const wrapper = mountComponent({ paddingInlineStart: '50' }); + + expect(wrapper.classes()).toContain('d-box--pis-50'); + }); + + it('applies paddingInlineEnd modifier class', () => { + const wrapper = mountComponent({ paddingInlineEnd: '75' }); + + expect(wrapper.classes()).toContain('d-box--pie-75'); + }); + + it('applies paddingBlock modifier class', () => { + const wrapper = mountComponent({ paddingBlock: '300' }); + + expect(wrapper.classes()).toContain('d-box--pbl-300'); + }); + + it('applies paddingBlockStart modifier class', () => { + const wrapper = mountComponent({ paddingBlockStart: '150' }); + + expect(wrapper.classes()).toContain('d-box--pbs-150'); + }); + + it('applies paddingBlockEnd modifier class', () => { + const wrapper = mountComponent({ paddingBlockEnd: '400' }); + + expect(wrapper.classes()).toContain('d-box--pbe-400'); + }); + + it('applies multiple padding classes simultaneously', () => { + const wrapper = mountComponent({ + padding: '200', + paddingInline: '100', + paddingBlockStart: '0', + }); + + expect(wrapper.classes()).toContain('d-box--p-200'); + expect(wrapper.classes()).toContain('d-box--pi-100'); + expect(wrapper.classes()).toContain('d-box--pbs-0'); + }); + + it('does not add padding class when prop is undefined', () => { + const wrapper = mountComponent(); + + const paddingClasses = wrapper.classes().filter(c => c.startsWith('d-box--p')); + expect(paddingClasses).toHaveLength(0); + }); + + // ── Surface ─────────────────────────────────────────────── + + it('applies surface modifier class', () => { + const wrapper = mountComponent({ surface: 'primary' }); + + expect(wrapper.classes()).toContain('d-box--surface-primary'); + }); + + it.each( + DT_BOX_SURFACE_VALUES.filter(v => v !== 'primary'), + )('applies surface modifier class for %s', (surface) => { + const wrapper = mountComponent({ surface }); + + expect(wrapper.classes()).toContain(`d-box--surface-${surface}`); + }); + + it('does not add surface class when prop is undefined', () => { + const wrapper = mountComponent(); + + const surfaceClasses = wrapper.classes().filter(c => c.startsWith('d-box--surface')); + expect(surfaceClasses).toHaveLength(0); + }); + + // ── Border color ─────────────────────────────────────────── + + it('applies borderColor modifier class', () => { + const wrapper = mountComponent({ borderColor: 'default' }); + + expect(wrapper.classes()).toContain('d-box--bc-default'); + }); + + it.each( + DT_BOX_BORDER_COLOR_VALUES.filter(v => v !== 'default'), + )('applies borderColor modifier class for %s', (borderColor) => { + const wrapper = mountComponent({ borderColor }); + + expect(wrapper.classes()).toContain(`d-box--bc-${borderColor}`); + }); + + it('applies default borderColor class when prop is not specified', () => { + const wrapper = mountComponent(); + + expect(wrapper.classes()).toContain('d-box--bc-default'); + }); + + // ── Border width ────────────────────────────────────────── + + it('applies borderWidth modifier class', () => { + const wrapper = mountComponent({ borderWidth: '100' }); + + expect(wrapper.classes()).toContain('d-box--bw-100'); + }); + + it.each( + DT_BOX_BORDER_WIDTH_VALUES.filter(v => v !== '100'), + )('applies borderWidth modifier class for %s', (borderWidth) => { + const wrapper = mountComponent({ borderWidth }); + + expect(wrapper.classes()).toContain(`d-box--bw-${borderWidth}`); + }); + + it('does not add borderWidth class when prop is undefined', () => { + const wrapper = mountComponent(); + + const bwClasses = wrapper.classes().filter(c => c.startsWith('d-box--bw')); + expect(bwClasses).toHaveLength(0); + }); + + // ── Directional border width ────────────────────────────── + + it.each([ + ['borderWidthInline', '100', 'd-box--bwi-100'], + ['borderWidthInlineStart', '150', 'd-box--bwis-150'], + ['borderWidthInlineEnd', '200', 'd-box--bwie-200'], + ['borderWidthBlock', '100', 'd-box--bwbl-100'], + ['borderWidthBlockStart', '150', 'd-box--bwbs-150'], + ['borderWidthBlockEnd', '200', 'd-box--bwbe-200'], + ])('applies %s modifier class', (prop, value, expectedClass) => { + const wrapper = mountComponent({ [prop]: value }); + + expect(wrapper.classes()).toContain(expectedClass); + }); + + // ── Border radius ───────────────────────────────────────── + + it('applies borderRadius modifier class', () => { + const wrapper = mountComponent({ borderRadius: '200' }); + + expect(wrapper.classes()).toContain('d-box--br-200'); + }); + + it.each( + DT_BOX_BORDER_RADIUS_VALUES.filter(v => v !== '200'), + )('applies borderRadius modifier class for %s', (borderRadius) => { + const wrapper = mountComponent({ borderRadius }); + + expect(wrapper.classes()).toContain(`d-box--br-${borderRadius}`); + }); + + it('does not add borderRadius class when prop is undefined', () => { + const wrapper = mountComponent(); + + const brClasses = wrapper.classes().filter(c => c.startsWith('d-box--br')); + expect(brClasses).toHaveLength(0); + }); + + // ── Shadow ──────────────────────────────────────────────── + + it('applies shadow modifier class', () => { + const wrapper = mountComponent({ shadow: 'small' }); + + expect(wrapper.classes()).toContain('d-box--shadow-small'); + }); + + it.each( + DT_BOX_SHADOW_VALUES.filter(v => v !== 'small'), + )('applies shadow modifier class for %s', (shadow) => { + const wrapper = mountComponent({ shadow }); + + expect(wrapper.classes()).toContain(`d-box--shadow-${shadow}`); + }); + + it('does not add shadow class when prop is undefined', () => { + const wrapper = mountComponent(); + + const shadowClasses = wrapper.classes().filter(c => c.startsWith('d-box--shadow')); + expect(shadowClasses).toHaveLength(0); + }); + + // ── Combined V2 props ────────────────────────────────────── + + it('applies multiple border and shadow classes simultaneously', () => { + const wrapper = mountComponent({ + borderColor: 'subtle', + borderWidth: '100', + borderRadius: '300', + shadow: 'card', + }); + + expect(wrapper.classes()).toContain('d-box--bc-subtle'); + expect(wrapper.classes()).toContain('d-box--bw-100'); + expect(wrapper.classes()).toContain('d-box--br-300'); + expect(wrapper.classes()).toContain('d-box--shadow-card'); + }); + + it('applies V1 and V2 props together', () => { + const wrapper = mountComponent({ + padding: '200', + surface: 'primary', + borderColor: 'default', + borderWidth: '100', + borderRadius: '400', + shadow: 'medium', + }); + + expect(wrapper.classes()).toContain('d-box--p-200'); + expect(wrapper.classes()).toContain('d-box--surface-primary'); + expect(wrapper.classes()).toContain('d-box--bc-default'); + expect(wrapper.classes()).toContain('d-box--bw-100'); + expect(wrapper.classes()).toContain('d-box--br-400'); + expect(wrapper.classes()).toContain('d-box--shadow-medium'); + }); + + // ── Sizing (layout tokens) ───────────────────────────────── + + it('applies inlineSize modifier class for layout token', () => { + const wrapper = mountComponent({ inlineSize: '300' }); + + expect(wrapper.classes()).toContain('d-box--is-300'); + }); + + it('applies blockSize modifier class for layout token', () => { + const wrapper = mountComponent({ blockSize: '500' }); + + expect(wrapper.classes()).toContain('d-box--bls-500'); + }); + + it('applies maxInlineSize modifier class for layout token', () => { + const wrapper = mountComponent({ maxInlineSize: '800' }); + + expect(wrapper.classes()).toContain('d-box--max-is-800'); + }); + + it('applies minBlockSize modifier class for layout token', () => { + const wrapper = mountComponent({ minBlockSize: '100' }); + + expect(wrapper.classes()).toContain('d-box--min-bls-100'); + }); + + it('applies minInlineSize modifier class for layout token', () => { + const wrapper = mountComponent({ minInlineSize: '200' }); + + expect(wrapper.classes()).toContain('d-box--min-is-200'); + }); + + it('applies maxBlockSize modifier class for layout token', () => { + const wrapper = mountComponent({ maxBlockSize: '600' }); + + expect(wrapper.classes()).toContain('d-box--max-bls-600'); + }); + + // ── Overflow ────────────────────────────────────────────── + + it('applies overflow modifier class', () => { + const wrapper = mountComponent({ overflow: 'hidden' }); + + expect(wrapper.classes()).toContain('d-box--of-hidden'); + }); + + it.each( + DT_BOX_OVERFLOW_VALUES.filter(v => v !== 'hidden'), + )('applies overflow modifier class for %s', (overflow) => { + const wrapper = mountComponent({ overflow }); + + expect(wrapper.classes()).toContain(`d-box--of-${overflow}`); + }); + + it('does not add overflow class when prop is undefined', () => { + const wrapper = mountComponent(); + + const ofClasses = wrapper.classes().filter(c => c.startsWith('d-box--of')); + expect(ofClasses).toHaveLength(0); + }); + + // ── Scrollbar ────────────────────────────────────────────── + + it('renders scrollbar viewport wrapper when scrollbar prop is set', () => { + const wrapper = mountComponent({ scrollbar: 'never' }); + + expect(wrapper.find('[data-qa="dt-box-scrollbar-content"]').exists()).toBe(true); + }); + + it('does not render scrollbar wrapper when scrollbar prop is undefined', () => { + const wrapper = mountComponent(); + + expect(wrapper.find('[data-qa="dt-box-scrollbar-content"]').exists()).toBe(false); + }); + + it('renders scrollbar wrapper when scrollbar is true', () => { + const wrapper = mountComponent({ scrollbar: true }); + + expect(wrapper.find('[data-qa="dt-box-scrollbar-content"]').exists()).toBe(true); + }); + + it('renders slot content inside scrollbar wrapper', () => { + const wrapper = mountComponent({ scrollbar: 'leave' }); + + expect(wrapper.find('[data-qa="dt-box-scrollbar-content"]').text()).toBe(slotContent); + }); + + // ── Attrs passthrough ───────────────────────────────────── + + it('passes class attr through to root element', () => { + const wrapper = mountComponent({}, { class: 'd-ps-sticky d-t0' }); + + expect(wrapper.classes()).toContain('d-ps-sticky'); + expect(wrapper.classes()).toContain('d-t0'); + }); + + it('passes id attr through to root element', () => { + const wrapper = mountComponent({}, { id: 'my-box' }); + + expect(wrapper.attributes('id')).toBe('my-box'); + }); + + it('passes aria attrs through to root element', () => { + const wrapper = mountComponent({}, { 'aria-label': 'Navigation' }); + + expect(wrapper.attributes('aria-label')).toBe('Navigation'); + }); +}); diff --git a/packages/dialtone-vue/components/box/box.vue b/packages/dialtone-vue/components/box/box.vue new file mode 100644 index 0000000000..b2ab7c8a20 --- /dev/null +++ b/packages/dialtone-vue/components/box/box.vue @@ -0,0 +1,292 @@ + + + + diff --git a/packages/dialtone-vue/components/box/box_constants.js b/packages/dialtone-vue/components/box/box_constants.js new file mode 100644 index 0000000000..beb9a10bc0 --- /dev/null +++ b/packages/dialtone-vue/components/box/box_constants.js @@ -0,0 +1,79 @@ +/** + * Valid `as` elements for the box component. + * @type {string[]} + */ +export const DT_BOX_AS_VALUES = ['div', 'span', 'section', 'article', 'aside', 'main', 'header', 'footer', 'nav', 'ul', 'ol', 'li', 'fieldset', 'form', 'figure']; + +/** + * Spacing token scale (shared across all padding props). + * @type {string[]} + */ +export const DT_BOX_SPACING_VALUES = ['0', '1', '25', '50', '75', '100', '125', '150', '175', '200', '250', '300', '350', '400', '450', '500', '525', '550', '600', '650', '700', '750', '800']; + +/** + * Surface color values (neutral + semantic + subtle/strong + opaque). + * @type {string[]} + */ +export const DT_BOX_SURFACE_VALUES = [ + 'primary', 'secondary', 'moderate', 'bold', 'strong', 'contrast', 'backdrop', + 'brand', 'info', 'positive', 'warning', 'critical', + 'brand-subtle', 'brand-strong', 'info-subtle', 'info-strong', + 'positive-subtle', 'positive-strong', 'warning-subtle', 'warning-strong', + 'critical-subtle', 'critical-strong', + 'primary-opaque', 'secondary-opaque', 'moderate-opaque', 'bold-opaque', + 'strong-opaque', 'contrast-opaque', + 'brand-opaque', 'brand-subtle-opaque', 'info-opaque', 'info-subtle-opaque', + 'positive-opaque', 'positive-subtle-opaque', 'warning-opaque', 'warning-subtle-opaque', + 'critical-opaque', 'critical-subtle-opaque', +]; + +/** + * Border color values (neutral + semantic + variants). + * @type {string[]} + */ +export const DT_BOX_BORDER_COLOR_VALUES = [ + 'transparent', 'subtle', 'default', 'moderate', 'bold', 'accent', 'focus', + 'brand', 'positive', 'warning', 'critical', + 'brand-subtle', 'brand-strong', 'positive-subtle', 'positive-strong', + 'warning-subtle', 'warning-strong', 'critical-subtle', 'critical-strong', +]; + +/** + * Border width values (maps to --dt-size-border-* tokens). + * @type {string[]} + */ +export const DT_BOX_BORDER_WIDTH_VALUES = ['0', '50', '100', '150', '200', '300', '400']; + +/** + * Border radius values (maps to --dt-size-radius-* tokens). + * @type {string[]} + */ +export const DT_BOX_BORDER_RADIUS_VALUES = ['0', '100', '200', '300', '350', '400', '450', '500', '600', 'pill', 'circle']; + +/** + * Shadow values (maps to --dt-shadow-* tokens). + * @type {string[]} + */ +export const DT_BOX_SHADOW_VALUES = ['small', 'medium', 'large', 'extra-large', 'card']; + +/** + * Layout token scale (for sizing props: inlineSize, blockSize, min/max variants). + * @type {string[]} + */ +export const DT_BOX_LAYOUT_VALUES = [ + '0', '25', '50', '75', '100', '200', '300', '400', '500', '600', '700', '800', '900', '1000', '1100', '1200', '1300', '1400', '1500', '1600', + '10-percent', '20-percent', '25-percent', '30-percent', '33-percent', '40-percent', '50-percent', + '60-percent', '66-percent', '70-percent', '75-percent', '80-percent', '90-percent', '95-percent', '100-percent', +]; + +/** + * Overflow values. + * @type {string[]} + */ +export const DT_BOX_OVERFLOW_VALUES = ['hidden', 'scroll', 'auto', 'clip', 'visible']; + +/** + * Scrollbar autoHide mode values (maps to OverlayScrollbars autoHide option). + * @type {string[]} + */ +export const DT_BOX_SCROLLBAR_VALUES = ['leave', 'scroll', 'move', 'never']; diff --git a/packages/dialtone-vue/components/box/box_variants.story.vue b/packages/dialtone-vue/components/box/box_variants.story.vue new file mode 100644 index 0000000000..def89e19ec --- /dev/null +++ b/packages/dialtone-vue/components/box/box_variants.story.vue @@ -0,0 +1,258 @@ + + + diff --git a/packages/dialtone-vue/components/box/index.js b/packages/dialtone-vue/components/box/index.js new file mode 100644 index 0000000000..29575616f4 --- /dev/null +++ b/packages/dialtone-vue/components/box/index.js @@ -0,0 +1,13 @@ +export { default as DtBox } from './box.vue'; +export { + DT_BOX_AS_VALUES, + DT_BOX_SPACING_VALUES, + DT_BOX_SURFACE_VALUES, + DT_BOX_BORDER_COLOR_VALUES, + DT_BOX_BORDER_WIDTH_VALUES, + DT_BOX_BORDER_RADIUS_VALUES, + DT_BOX_SHADOW_VALUES, + DT_BOX_LAYOUT_VALUES, + DT_BOX_OVERFLOW_VALUES, + DT_BOX_SCROLLBAR_VALUES, +} from './box_constants.js'; diff --git a/packages/dialtone-vue/components/box/validators.js b/packages/dialtone-vue/components/box/validators.js new file mode 100644 index 0000000000..c59acda717 --- /dev/null +++ b/packages/dialtone-vue/components/box/validators.js @@ -0,0 +1,23 @@ +import { + DT_BOX_AS_VALUES, + DT_BOX_SPACING_VALUES, + DT_BOX_SURFACE_VALUES, + DT_BOX_BORDER_COLOR_VALUES, + DT_BOX_BORDER_WIDTH_VALUES, + DT_BOX_BORDER_RADIUS_VALUES, + DT_BOX_SHADOW_VALUES, + DT_BOX_LAYOUT_VALUES, + DT_BOX_OVERFLOW_VALUES, + DT_BOX_SCROLLBAR_VALUES, +} from './box_constants.js'; + +export const asValidator = (value) => DT_BOX_AS_VALUES.includes(value); +export const spacingValidator = (value) => DT_BOX_SPACING_VALUES.includes(String(value)); +export const surfaceValidator = (value) => DT_BOX_SURFACE_VALUES.includes(value); +export const borderColorValidator = (value) => DT_BOX_BORDER_COLOR_VALUES.includes(value); +export const borderWidthValidator = (value) => DT_BOX_BORDER_WIDTH_VALUES.includes(String(value)); +export const borderRadiusValidator = (value) => DT_BOX_BORDER_RADIUS_VALUES.includes(String(value)); +export const shadowValidator = (value) => DT_BOX_SHADOW_VALUES.includes(value); +export const layoutValidator = (value) => DT_BOX_LAYOUT_VALUES.includes(String(value)); +export const overflowValidator = (value) => DT_BOX_OVERFLOW_VALUES.includes(value); +export const scrollbarValidator = (value) => value === true || DT_BOX_SCROLLBAR_VALUES.includes(value); diff --git a/packages/dialtone-vue/components/input/input.test.js b/packages/dialtone-vue/components/input/input.test.js index 5bb81d5c2f..1a0768f37f 100644 --- a/packages/dialtone-vue/components/input/input.test.js +++ b/packages/dialtone-vue/components/input/input.test.js @@ -233,11 +233,11 @@ describe('DtInput tests', () => { describe('When an inputClass prop is provided', () => { it('Should apply the class to the input element.', () => { - mockProps = { inputClass: 'd-fc-success' }; + mockProps = { inputClass: 'd-fc-positive' }; updateWrapper(); - expect(nativeInput.classes('d-fc-success')).toBe(true); + expect(nativeInput.classes('d-fc-positive')).toBe(true); }); }); @@ -550,13 +550,13 @@ describe('DtInput tests', () => { describe('When labelClass is provided', () => { it('should apply custom class to the label', () => { - mockProps = { label: 'Label', labelClass: 'd-fc-success' }; + mockProps = { label: 'Label', labelClass: 'd-fc-positive' }; updateWrapper(); const labelEl = wrapper.find('[data-qa="dt-input-label"]'); - expect(labelEl.classes('d-fc-success')).toBe(true); + expect(labelEl.classes('d-fc-positive')).toBe(true); }); }); diff --git a/packages/dialtone-vue/components/mode_island/mode_island_default.story.vue b/packages/dialtone-vue/components/mode_island/mode_island_default.story.vue index b35f511e40..6c0aa82d82 100644 --- a/packages/dialtone-vue/components/mode_island/mode_island_default.story.vue +++ b/packages/dialtone-vue/components/mode_island/mode_island_default.story.vue @@ -15,8 +15,8 @@ Tertiary Foreground - - Success Foreground + + Positive Foreground Critical Foreground diff --git a/packages/dialtone-vue/directives/mode_directive/mode_directive_default.story.vue b/packages/dialtone-vue/directives/mode_directive/mode_directive_default.story.vue index 630005ba6f..7f439578e8 100644 --- a/packages/dialtone-vue/directives/mode_directive/mode_directive_default.story.vue +++ b/packages/dialtone-vue/directives/mode_directive/mode_directive_default.story.vue @@ -31,7 +31,7 @@ > Primary Tertiary - Success + Positive Critical @@ -53,7 +53,7 @@ > Primary Tertiary - Success + Positive Critical diff --git a/packages/dialtone-vue/index.js b/packages/dialtone-vue/index.js index ab076eb844..c6601545fb 100644 --- a/packages/dialtone-vue/index.js +++ b/packages/dialtone-vue/index.js @@ -10,6 +10,7 @@ export * from './common/emoji'; export * from './components/avatar'; export * from './components/badge'; export * from './components/banner'; +export * from './components/box'; export * from './components/breadcrumbs'; export * from './components/button'; export * from './components/button_group'; diff --git a/packages/dialtone-vue/prototypes/dialpad/leftbar/dialpad_leftbar.vue b/packages/dialtone-vue/prototypes/dialpad/leftbar/dialpad_leftbar.vue index 01415580ad..fed471d4cb 100644 --- a/packages/dialtone-vue/prototypes/dialpad/leftbar/dialpad_leftbar.vue +++ b/packages/dialtone-vue/prototypes/dialpad/leftbar/dialpad_leftbar.vue @@ -47,7 +47,7 @@