diff --git a/.storybook/main.ts b/.storybook/main.ts index 9d92ed2e2e..2e40a5715c 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,10 +1,59 @@ import type { StorybookConfig } from '@storybook/html-vite'; +import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import remarkGfm from 'remark-gfm'; +import type { Plugin } from 'vite'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, '..'); +const distFolder = path.join(repoRoot, 'dist'); + +/** + * Dev-bundle fallback for /osui/*. + * + * `preview-head.html` (and the Theme toolbar toggle) load the PRODUCTION file + * names (`ODC.OutSystemsUI.{css,js}`), but `npm run dev -- --target ODC` wipes + * `dist/` and emits only `dev.`-prefixed files. Without this fallback, running + * Storybook against a dev watch build 404s the whole OUI bundle. + * + * When a requested `/osui/` is missing from `dist/` but `dist/dev.` + * exists, serve the dev file instead. Static-dir serving (sirv) falls through + * to the Vite middleware chain on a miss, which is where this plugin runs. + * Dev-server only — `storybook build` copies staticDirs verbatim and expects a + * prior production build, as before. + */ +function osuiDevBundleFallback(): Plugin { + const contentTypes: Record = { + '.css': 'text/css', + '.js': 'text/javascript', + '.map': 'application/json', + }; + return { + name: 'osui-dev-bundle-fallback', + configureServer(server) { + server.middlewares.use((req, res, next) => { + const url = (req.url ?? '').split('?')[0]; + if (!url.startsWith('/osui/')) { + return next(); + } + const requested = url.slice('/osui/'.length); + // Only flat file names — no nested paths, no traversal, not already dev-prefixed. + if (requested === '' || /[/\\]/.test(requested) || requested.includes('..') || requested.startsWith('dev.')) { + return next(); + } + const prodPath = path.join(distFolder, requested); + const devPath = path.join(distFolder, `dev.${requested}`); + if (fs.existsSync(prodPath) || !fs.existsSync(devPath)) { + return next(); + } + res.setHeader('Content-Type', contentTypes[path.extname(requested)] ?? 'application/octet-stream'); + res.setHeader('Cache-Control', 'no-store'); + fs.createReadStream(devPath).pipe(res); + }); + }, + }; +} /** * Storybook for OutSystems UI. @@ -17,8 +66,10 @@ const repoRoot = path.resolve(__dirname, '..'); * calls its public `Create(id, configs)` API — exactly what OutSystems Service * Studio does at runtime, transcribed to high-code. * - * Prerequisite: run `npm run build:osui` (or `npm run dev -- --target ODC`) - * once so `dist/ODC.OutSystemsUI.{js,css}` exist. They are served from `/osui`. + * Prerequisite: build the library so `dist/` is populated — either `npm run build` + * (production names, `ODC.OutSystemsUI.{js,css}`) or `npm run dev -- --target ODC` + * (dev watch, `dev.`-prefixed names — resolved by the osuiDevBundleFallback plugin + * above). Files are served from `/osui`. */ const config: StorybookConfig = { stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'], @@ -67,6 +118,10 @@ const config: StorybookConfig = { { from: path.join(repoRoot, 'node_modules/@floating-ui/dom/dist'), to: '/vendor/floating-ui-dom' }, { from: path.join(repoRoot, 'node_modules/virtual-select-plugin/dist'), to: '/vendor/virtual-select' }, ], + viteFinal(viteConfig) { + viteConfig.plugins = [...(viteConfig.plugins ?? []), osuiDevBundleFallback()]; + return viteConfig; + }, }; export default config; diff --git a/gulp/ProjectSpecs/ScssStructure/Keyframes.js b/gulp/ProjectSpecs/ScssStructure/Keyframes.js index bd522df958..7ebc656cc0 100644 --- a/gulp/ProjectSpecs/ScssStructure/Keyframes.js +++ b/gulp/ProjectSpecs/ScssStructure/Keyframes.js @@ -14,10 +14,6 @@ const sectionInfo = { "name": "Button Loading", "path": "07-keyframes/btn-loading" }, - { - "name": "List Item", - "path": "07-keyframes/list-item" - }, { "name": "Feedback Message", "path": "07-keyframes/feedback-message" diff --git a/src/scripts/OutSystems/OSUI/Utils/ButtonsEffect.ts b/src/scripts/OutSystems/OSUI/Utils/ButtonsEffect.ts index 230f4f4a66..e50e2c0f91 100644 --- a/src/scripts/OutSystems/OSUI/Utils/ButtonsEffect.ts +++ b/src/scripts/OutSystems/OSUI/Utils/ButtonsEffect.ts @@ -7,8 +7,9 @@ namespace OutSystems.OSUI.Utils { return; } - const foundElement = - _hasSomeParentTheClass(target, 'list-item') || _hasSomeParentTheClass(target, 'bottom-bar-item'); + // 'list-item' is intentionally not handled here: the List Item click feedback is now a + // pure CSS `:active` background (see src/scss/03-widgets/_list-item.scss). + const foundElement = _hasSomeParentTheClass(target, 'bottom-bar-item'); if (foundElement) { _clickEffect(foundElement); diff --git a/src/scss/01-foundations/_root.scss b/src/scss/01-foundations/_root.scss index f9626445da..e00f4c28b3 100644 --- a/src/scss/01-foundations/_root.scss +++ b/src/scss/01-foundations/_root.scss @@ -127,7 +127,7 @@ --osui-motion-duration-130: 130ms; // future: --token-transition-time-130 --osui-motion-duration-180: 180ms; // future: --token-transition-time-180 --osui-motion-duration-400: 400ms; // future: --token-transition-time-400 - --osui-bg-surface-hover: #{variables.$token-primitives-neutral-100}; // future: --token-bg-surface-hover (#f3f3f3) + --osui-bg-surface-hover: #{variables.$token-bg-neutral-subtlest-hover}; --osui-bg-surface-active: color-mix( in srgb, #{variables.$token-semantics-primary-base} 8%, diff --git a/src/scss/03-widgets/_list-item.scss b/src/scss/03-widgets/_list-item.scss index 64e8860139..7d28f59fd1 100644 --- a/src/scss/03-widgets/_list-item.scss +++ b/src/scss/03-widgets/_list-item.scss @@ -11,41 +11,19 @@ --osui-list-item-border-color: var(--color-border); --osui-list-item-background: var(--color-background-surface); --osui-list-item-hover-background: var(--osui-bg-surface-hover); - --osui-list-item-selected-background: var(--color-primary-selected); + --osui-list-item-active-background: #{variables.$token-bg-neutral-subtlest-press}; + --osui-list-item-selected-background: var(--osui-list-item-background); + --osui-list-item-selected-icon-color: #{variables.$token-icon-select}; // ─────────────────────────────────────────────────────────────────── border-bottom: variables.$token-border-size-025 solid var(--osui-list-item-border-color); - overflow: hidden; - padding: variables.$token-scale-300 variables.$token-scale-600; + padding: variables.$token-space-300 variables.$token-space-600; position: relative; transition: background-color variables.$token-transition-time-100 variables.$token-transition-curve-base; &:last-of-type { border-bottom: none; } - - &.list-item-no-click-effect { - .scale-animation { - // !important should be in use in order to grant the expected behaviour when this class is assigned! - background: transparent !important; - opacity: 0 !important; - } - } - - .scale-animation { - animation: list-item-scale-animation variables.$token-transition-time-1500; - background-color: color-mix(in srgb, var(--color-text) 10%, transparent); - border-radius: var(--border-radius-rounded); - display: block; - height: 10px; // design-spec: fixed ripple size - left: 50%; - pointer-events: none; - position: absolute; - top: 50%; - width: 10px; // design-spec: fixed ripple size - will-change: transform; - z-index: var(--layer-screen); - } } /// @@ -53,6 +31,11 @@ .list-item { background-color: var(--osui-list-item-background); cursor: pointer; + + // Pressed state, on every device — replaces the legacy ripple animation. + &:active { + background-color: var(--osui-list-item-active-background); + } } } @@ -68,7 +51,8 @@ .desktop { .list { .list-item:hover { - &:not(.list-item-no-hover):not(.list-item-selected) { + // `:not(:active)` keeps this higher-specificity rule from masking the pressed state. + &:not(.list-item-no-hover):not(.list-item-selected):not(:active) { background-color: var(--osui-list-item-hover-background); } } @@ -80,19 +64,16 @@ .tablet.landscape { .list { .list-item-selected { - background-color: color-mix(in srgb, #{variables.$token-semantics-primary-base} 10%, #{variables.$token-bg-surface-default}); + background-color: var(--osui-list-item-selected-background); - .list-item-content { - &-title { - color: var(--color-text); - } - - &-text { - color: var(--color-text-subtlest); - } + // Restated here so the pressed state also wins over the selected background. + &:active { + background-color: var(--osui-list-item-active-background); + } + .list-item-content { &-right .icon { - color: var(--osui-list-item-selected-background); + color: var(--osui-list-item-selected-icon-color); } } } diff --git a/src/scss/04-patterns/02-content/_chat-message.scss b/src/scss/04-patterns/02-content/_chat-message.scss index f54fe9349c..d8711f50d5 100644 --- a/src/scss/04-patterns/02-content/_chat-message.scss +++ b/src/scss/04-patterns/02-content/_chat-message.scss @@ -12,6 +12,7 @@ --osui-chat-message-border-radius: #{variables.$token-border-radius-200}; --osui-chat-message-sent-background: var(--color-primary); --osui-chat-message-sent-color: var(--color-text-inverse); + --osui-chat-message-status-color: #{variables.$token-text-subtlest}; // ─────────────────────────────────────────────────────────────────── display: flex; @@ -21,26 +22,25 @@ .chat { &-photo { - margin-left: variables.$token-scale-400; + margin-left: variables.$token-space-400; margin-right: variables.$token-scale-0; } &-message { background-color: var(--osui-chat-message-sent-background); color: var(--osui-chat-message-sent-color); - text-align: right; } } } &-photo { - margin-right: variables.$token-scale-400; + margin-right: variables.$token-space-400; img { border-radius: var(--border-radius-rounded); - height: variables.$token-scale-1000; + height: variables.$token-scale-800; object-fit: cover; - width: variables.$token-scale-1000; + width: variables.$token-scale-800; } } @@ -50,14 +50,15 @@ display: flex; flex-direction: column; max-width: 600px; - padding: variables.$token-scale-400; + padding: variables.$token-space-300; position: relative; word-break: break-word; &-status { + color: var(--osui-chat-message-status-color); display: block; - font-size: variables.$token-font-size-300; - margin-top: variables.$token-scale-200; + font-size: variables.$token-font-size-275; + margin-top: variables.$token-space-100; &.hidden { display: none; @@ -74,13 +75,13 @@ .chat { &-photo { margin-left: variables.$token-scale-0; - margin-right: variables.$token-scale-400; + margin-right: variables.$token-space-400; } } } &-photo { - margin-left: variables.$token-scale-400; + margin-left: variables.$token-space-400; margin-right: 0; } } diff --git a/src/scss/04-patterns/02-content/_floating-content.scss b/src/scss/04-patterns/02-content/_floating-content.scss index 1dd013d0f4..67198b3ac8 100644 --- a/src/scss/04-patterns/02-content/_floating-content.scss +++ b/src/scss/04-patterns/02-content/_floating-content.scss @@ -24,7 +24,7 @@ &-height { margin-top: 0; - top: calc(var(--size-header) + variables.$token-scale-400 * 2); + top: calc(var(--size-header) + variables.$token-space-400 * 2); &.absolute-top { top: var(--os-safe-area-top); @@ -53,19 +53,19 @@ &-top { left: 50%; margin-top: 0; - top: calc(var(--size-header) + variables.$token-scale-400 * 2); + top: calc(var(--size-header) + variables.$token-space-400 * 2); transform: translateX(-50%); &-left { left: 0; margin-top: 0; - top: calc(var(--size-header) + variables.$token-scale-400 * 2); + top: calc(var(--size-header) + variables.$token-space-400 * 2); } &-right { margin-top: 0; right: 0; - top: calc(var(--size-header) + variables.$token-scale-400 * 2); + top: calc(var(--size-header) + variables.$token-space-400 * 2); } } @@ -154,22 +154,22 @@ } &-margin { - margin: variables.$token-scale-800; + margin: variables.$token-space-800; &.floating-content { &-center { - left: calc(50% - variables.$token-scale-800); - top: calc(50% - variables.$token-scale-800); + left: calc(50% - variables.$token-space-800); + top: calc(50% - variables.$token-space-800); } &-left, &-right { - top: calc(50% - variables.$token-scale-800); + top: calc(50% - variables.$token-space-800); } &-top, &-bottom, &-center { - left: calc(50% - variables.$token-scale-800); + left: calc(50% - variables.$token-space-800); } } } @@ -192,7 +192,7 @@ } &.floating-content-full-height.floating-content-margin { - margin: variables.$token-scale-800; + margin: variables.$token-space-800; } } } diff --git a/src/scss/04-patterns/02-content/_list-item-content.scss b/src/scss/04-patterns/02-content/_list-item-content.scss index 1914470e7c..40bfaae1ba 100644 --- a/src/scss/04-patterns/02-content/_list-item-content.scss +++ b/src/scss/04-patterns/02-content/_list-item-content.scss @@ -9,7 +9,9 @@ .list-item-content { // ─── Component CSS API ───────────────────────────────────────────── --osui-list-item-content-title-color: var(--color-text); - --osui-list-item-content-text-color: #{variables.$token-primitives-neutral-700}; + --osui-list-item-content-text-color: #{variables.$token-text-subtlest}; + --osui-list-item-content-icon-size: #{variables.$token-scale-500}; + --osui-list-item-content-icon-color: #{variables.$token-icon-subtlest}; // ─────────────────────────────────────────────────────────────────── align-items: center; @@ -17,7 +19,7 @@ &-left { max-width: 120px; - padding-right: variables.$token-scale-400; + padding-right: variables.$token-space-400; } &-center { @@ -26,7 +28,16 @@ } &-right { - padding-left: variables.$token-scale-400; + padding-left: variables.$token-space-600; + + // Trailing icon renders in a 20px box (Figma CaretRight); `line-height: 1` + // keeps the glyph box square instead of inheriting the row's line-height. + // The selected row overrides the color via --osui-list-item-selected-icon-color. + .icon { + color: var(--osui-list-item-content-icon-color); + font-size: var(--osui-list-item-content-icon-size); + line-height: 1; + } } &-title, @@ -38,12 +49,14 @@ &-title { color: var(--osui-list-item-content-title-color); - line-height: variables.$token-font-line-height-500; + font-size: variables.$token-font-size-400; font-weight: variables.$token-font-weight-semi-bold; + line-height: variables.$token-font-line-height-600; } &-text { color: var(--osui-list-item-content-text-color); + font-size: variables.$token-font-size-350; } } @@ -51,55 +64,40 @@ .layout-native { .list-item-content { &-left { - padding-right: variables.$token-scale-400; + padding-right: variables.$token-space-400; } &-right { - padding-left: variables.$token-scale-400; + padding-left: variables.$token-space-600; } } } -// Responsive -------------------------------------------------------------------- -/// -.tablet { - .list-item-content-title { - font-size: calc(variables.$token-font-size-500 - 1px); - } -} - -/// -.phone { - .list-item-content-title { - font-size: calc(variables.$token-font-size-500 - 2px); - } -} - // IsRTL ------------------------------------------------------------------------- /// .is-rtl { .list-item-content { &-left { - padding-left: variables.$token-scale-400; + padding-left: variables.$token-space-400; padding-right: 0; } &-right { padding-left: 0; - padding-right: variables.$token-scale-400; + padding-right: variables.$token-space-600; } } .layout-native { .list-item-content { &-left { - padding-left: variables.$token-scale-400; + padding-left: variables.$token-space-400; padding-right: 0; } &-right { padding-left: 0; - padding-right: variables.$token-scale-400; + padding-right: variables.$token-space-600; } } } diff --git a/src/scss/04-patterns/02-content/_section.scss b/src/scss/04-patterns/02-content/_section.scss index b7d9789aa2..2d9980fddf 100644 --- a/src/scss/04-patterns/02-content/_section.scss +++ b/src/scss/04-patterns/02-content/_section.scss @@ -11,6 +11,7 @@ // ─── Component CSS API ───────────────────────────────────────────── --osui-section-title-color: var(--color-text); --osui-section-title-border-color: var(--color-border); + --osui-section-content-color: var(--color-text-subtlest); // ─────────────────────────────────────────────────────────────────── &-title { @@ -18,18 +19,27 @@ border-bottom: variables.$token-border-size-025 solid var(--osui-section-title-border-color); color: var(--osui-section-title-color); font-size: variables.$token-font-size-500; - font-weight: variables.$token-font-weight-semi-bold; - padding: variables.$token-scale-0 variables.$token-scale-0 variables.$token-scale-200 variables.$token-scale-0; + font-weight: variables.$token-font-weight-medium; + padding: variables.$token-scale-0 variables.$token-scale-0 variables.$token-space-200 variables.$token-scale-0; position: relative; text-transform: none; width: 100%; } &-content { - padding: variables.$token-scale-200 variables.$token-scale-0 variables.$token-scale-0; + color: var(--osui-section-content-color); + padding: variables.$token-space-200 variables.$token-scale-0 variables.$token-scale-0; } &-group { + // ─── Component CSS API ───────────────────────────────────────────── + --osui-section-group-gap: #{variables.$token-space-400}; + // ─────────────────────────────────────────────────────────────────── + + display: flex; + flex-direction: column; + gap: var(--osui-section-group-gap); + &.is--sticky { .section-title { position: -webkit-sticky; //Fix for older iPhones diff --git a/src/scss/04-patterns/02-content/_tag.scss b/src/scss/04-patterns/02-content/_tag.scss index ce30550eeb..163059f2af 100644 --- a/src/scss/04-patterns/02-content/_tag.scss +++ b/src/scss/04-patterns/02-content/_tag.scss @@ -1,3 +1,5 @@ +@use "sass:map"; +@use '../../00-abstract/index' as *; @use "../../tokens/variables"; /* Patterns - Content - Tag */ @@ -5,12 +7,31 @@ /// @group Patterns-Tag /// Patterns - Content - Tag +// Per-family IsLight=true text colors, per the Figma Tag spec: families mapped +// to a semantic read the semantic text token, extended families read the +// matching text-extended-* token (cyan→aqua, grape→purple per token naming). +$osui-tag-lightest-text-colors: ( + 'red': variables.$token-text-danger, + 'orange': variables.$token-text-extended-orange, + 'yellow': variables.$token-text-warning, + 'lime': variables.$token-text-extended-lime, + 'green': variables.$token-text-success, + 'teal': variables.$token-text-extended-teal, + 'cyan': variables.$token-text-extended-aqua, + 'blue': variables.$token-text-info, + 'indigo': variables.$token-text-extended-indigo, + 'violet': variables.$token-text-extended-violet, + 'grape': variables.$token-text-extended-purple, + 'pink': variables.$token-text-extended-pink, +); + /// .tag { // ─── Component CSS API ───────────────────────────────────────────── --osui-tag-color: var(--color-text-inverse); --osui-tag-primary-color: var(--color-primary); --osui-tag-on-light-color: var(--color-text); + --osui-tag-soft-border-radius: #{variables.$token-border-radius-100}; // ─────────────────────────────────────────────────────────────────── align-items: center; @@ -18,55 +39,141 @@ display: inline-flex; font-size: variables.$token-font-size-300; font-weight: variables.$token-font-weight-medium; - gap: variables.$token-scale-100; - min-height: variables.$token-scale-500; + gap: variables.$token-space-200; justify-content: center; - letter-spacing: 0.01em; // design-spec: no letter-spacing token in system line-height: variables.$token-font-line-height-full; + min-height: variables.$token-scale-500; min-width: unset; - padding: variables.$token-scale-0 variables.$token-scale-200; + padding: variables.$token-space-0 variables.$token-space-300; word-break: normal; + // Figma Tag spec: Soft shape uses the 4px radius token (border/border-radius/100), + // narrower than the global 8px .border-radius-soft utility. + &.border-radius-soft { + border-radius: var(--osui-tag-soft-border-radius); + } + &.tag { &-small { - font-size: var(--osui-font-size-200); min-height: variables.$token-scale-400; - padding: variables.$token-scale-0 variables.$token-scale-150; + min-width: variables.$token-scale-400; + padding: variables.$token-space-0 variables.$token-space-150; } &-medium { font-size: variables.$token-font-size-350; min-height: variables.$token-scale-700; - padding: variables.$token-scale-0 variables.$token-scale-300; + padding: variables.$token-space-0 variables.$token-space-300; } } &.background { &-transparent { color: var(--osui-tag-primary-color); - } - &-neutral { - &-0 { - color: var(--osui-tag-primary-color); + &-lightest { + color: variables.$token-text-inverse; } + } + &-neutral { + &-0, &-1, &-2, &-3, - &-4 { - color: var(--osui-tag-on-light-color); + &-4, + &-5 { + background-color: variables.$token-bg-neutral-base-default; + color: variables.$token-text-default; } + + &-6, + &-7, + &-8, + &-9, + &-10 { + background-color: variables.$token-bg-neutral-bold-default; + color: variables.$token-text-inverse; + } + + &-0-lightest, + &-1-lightest, + &-2-lightest, + &-3-lightest, + &-4-lightest, + &-5-lightest { + background-color: variables.$token-bg-neutral-subtle-default; + color: variables.$token-text-subtle; + } + + &-6-lightest, + &-7-lightest, + &-8-lightest, + &-9-lightest, + &-10-lightest { + background-color: variables.$token-bg-neutral-base-default; + color: variables.$token-text-default; + } + } + + &-error { + background-color: variables.$token-bg-danger-base-default; + } + + &-success { + background-color: variables.$token-bg-success-base-default; } + + &-info { + background-color: variables.$token-bg-info-base-default; + } + + &-warning { + background-color: variables.$token-bg-warning-base-default; + } + + // Warning backgrounds are light enough to need dark text (Figma). + &-warning, + &-yellow { + color: var(--osui-tag-on-light-color); + } + } + + // IsLight=true — semantic colors render `background-{semantic}-light`; + // the utility class provides the subtle background, only text is set here. + &.background-error-light { + color: variables.$token-text-danger; + } + + &.background-success-light { + color: variables.$token-text-success; } - // IsLight=true renders `background-{color}-lightest`. The brand and neutral - // light variants sit on a plain (untinted) surface, so their text must invert - // to the dark on-light color to stay readable — palette colors keep their own - // darker text via the `text-{color}-darker` class. - &.background-primary-lightest, - &.background-secondary-lightest, - &[class*='background-neutral'][class*='-lightest'] { + &.background-info-light { + color: variables.$token-text-info; + } + + &.background-warning-light { + color: variables.$token-text-warning; + } + + // The background-primary-lightest utility is still surface-white (pending + // theme-layer review), so the Figma subtle background is set locally. + &.background-primary-lightest { + background-color: variables.$token-bg-primary-subtle-default; + color: variables.$token-text-primary; + } + + &.background-secondary-lightest { + background-color: variables.$token-bg-neutral-base-default; color: var(--osui-tag-on-light-color); } + + // Extended palette IsLight=true — background comes from the token-backed + // background-{color}-lightest utility; only the per-family text token is set. + @each $color, $variations in $osui-colors-extended { + &.background-#{$color}-lightest { + color: #{map.get($osui-tag-lightest-text-colors, $color)}; + } + } } diff --git a/src/scss/04-patterns/02-content/_user-avatar.scss b/src/scss/04-patterns/02-content/_user-avatar.scss index 9812ae4b23..de1d068546 100644 --- a/src/scss/04-patterns/02-content/_user-avatar.scss +++ b/src/scss/04-patterns/02-content/_user-avatar.scss @@ -1,3 +1,5 @@ +@use "sass:map"; +@use '../../00-abstract/index' as *; @use "../../tokens/variables"; /* Patterns - Content - User Avatar */ @@ -5,18 +7,37 @@ /// @group Patterns-User_Avatar /// Patterns - Content - User Avatar +// Per-family IsLight=true text colors, per the Figma UserAvatar spec (same +// matrix as Tag): families mapped to a semantic read the semantic text token, +// extended families read text-extended-* (cyan→aqua, grape→purple). +$osui-avatar-lightest-text-colors: ( + 'red': variables.$token-text-danger, + 'orange': variables.$token-text-extended-orange, + 'yellow': variables.$token-text-warning, + 'lime': variables.$token-text-extended-lime, + 'green': variables.$token-text-success, + 'teal': variables.$token-text-extended-teal, + 'cyan': variables.$token-text-extended-aqua, + 'blue': variables.$token-text-info, + 'indigo': variables.$token-text-extended-indigo, + 'violet': variables.$token-text-extended-violet, + 'grape': variables.$token-text-extended-purple, + 'pink': variables.$token-text-extended-pink, +); + /// .avatar { // ─── Component CSS API ───────────────────────────────────────────── - --osui-avatar-color: var(--color-text-inverse); - --osui-avatar-primary-color: var(--color-primary); + --osui-avatar-color: var(--color-text-inverse); + --osui-avatar-primary-color: var(--color-primary); --osui-avatar-on-light-color: var(--color-text); + --osui-avatar-soft-border-radius: #{variables.$token-border-radius-100}; // ─────────────────────────────────────────────────────────────────── align-items: center; color: var(--osui-avatar-color); display: inline-flex; - font-size: variables.$token-font-size-300; + font-size: variables.$token-font-size-350; font-weight: variables.$token-font-weight-semi-bold; height: variables.$token-scale-800; justify-content: center; @@ -24,45 +45,126 @@ text-transform: uppercase; width: variables.$token-scale-800; + // Figma UserAvatar spec: Soft shape uses the 4px radius token + // (border/border-radius/100), narrower than the global 8px utility. + &.border-radius-soft { + border-radius: var(--osui-avatar-soft-border-radius); + } + + &.avatar-small { + font-size: variables.$token-font-size-300; + font-weight: variables.$token-font-weight-medium; + height: variables.$token-scale-600; + width: variables.$token-scale-600; + } + + &.avatar-medium { + font-size: variables.$token-font-size-400; + font-weight: variables.$token-font-weight-medium; + height: variables.$token-scale-1000; + width: variables.$token-scale-1000; + } + &.background { &-transparent { color: var(--osui-avatar-primary-color); - } - &-neutral { - &-0 { - color: var(--osui-avatar-primary-color); + &-lightest { + color: variables.$token-text-inverse; } + } + &-neutral { + &-0, &-1, &-2, &-3, - &-4 { - color: var(--osui-avatar-on-light-color); + &-4, + &-5 { + background-color: variables.$token-bg-neutral-base-default; + color: variables.$token-text-default; + } + + &-6, + &-7, + &-8, + &-9, + &-10 { + background-color: variables.$token-bg-neutral-bold-default; + color: variables.$token-text-inverse; + } + + &-0-lightest, + &-1-lightest, + &-2-lightest, + &-3-lightest, + &-4-lightest, + &-5-lightest { + background-color: variables.$token-bg-neutral-subtle-default; + color: variables.$token-text-subtle; } + + &-6-lightest, + &-7-lightest, + &-8-lightest, + &-9-lightest, + &-10-lightest { + background-color: variables.$token-bg-neutral-base-default; + color: variables.$token-text-default; + } + } + + // Warning backgrounds are light enough to need dark text (Figma). + &-warning, + &-yellow { + color: var(--osui-avatar-on-light-color); } } - // IsLight=true renders `background-{color}-lightest`. The brand and neutral - // light variants sit on a plain (untinted) surface, so their text must invert - // to the dark on-light color to stay readable — palette colors keep their own - // darker text via the `text-{color}-darker` class. - &.background-primary-lightest, - &.background-secondary-lightest, - &[class*='background-neutral'][class*='-lightest'] { + // IsLight=true — semantic colors render `background-{semantic}-light`; + // the utility class provides the subtle background, only text is set here. + &.background-error-light { + color: variables.$token-text-danger; + } + + &.background-success-light { + color: variables.$token-text-success; + } + + &.background-info-light { + color: variables.$token-text-info; + } + + &.background-warning-light { + color: variables.$token-text-warning; + } + + // Per Figma, the light primary avatar keeps the plain white background the + // background-primary-lightest utility already provides (bg/neutral/subtlest) + // — only the text follows the brand primary color. + &.background-primary-lightest { + color: var(--osui-avatar-primary-color); + } + + &.background-secondary-lightest { + background-color: variables.$token-bg-neutral-base-default; color: var(--osui-avatar-on-light-color); } - &.avatar-small { - font-size: var(--osui-font-size-200); - height: variables.$token-scale-600; - width: variables.$token-scale-600; + // Extended palette IsLight=true — background comes from the token-backed + // background-{color}-lightest utility; only the per-family text token is set. + @each $color, $variations in $osui-colors-extended { + &.background-#{$color}-lightest { + color: #{map.get($osui-avatar-lightest-text-colors, $color)}; + } } - &.avatar-medium { - font-size: variables.$token-font-size-400; - height: variables.$token-scale-1000; - width: variables.$token-scale-1000; + // Type=Image — the photo fills the avatar box and follows its shape. + img { + border-radius: inherit; + height: 100%; + object-fit: cover; + width: 100%; } span { diff --git a/src/scss/04-patterns/02-content/flip-content/_flipcontent.scss b/src/scss/04-patterns/02-content/flip-content/_flipcontent.scss index 9c13d032c1..816a5544ee 100644 --- a/src/scss/04-patterns/02-content/flip-content/_flipcontent.scss +++ b/src/scss/04-patterns/02-content/flip-content/_flipcontent.scss @@ -76,6 +76,7 @@ &__back { position: absolute; width: 100%; + transform: rotateY(180deg); // Service Studio preview & { diff --git a/src/scss/04-patterns/03-interaction/tooltip/_tooltip.scss b/src/scss/04-patterns/03-interaction/tooltip/_tooltip.scss index eb8ace5b07..5d7d06d615 100644 --- a/src/scss/04-patterns/03-interaction/tooltip/_tooltip.scss +++ b/src/scss/04-patterns/03-interaction/tooltip/_tooltip.scss @@ -11,10 +11,11 @@ --osui-tooltip-background: #{variables.$token-bg-neutral-boldest-default}; --osui-tooltip-color: var(--color-text-inverse); --osui-tooltip-border-radius: #{variables.$token-border-radius-100}; - --osui-tooltip-padding: #{variables.$token-scale-200} #{variables.$token-scale-400}; + --osui-tooltip-padding: #{variables.$token-space-200} #{variables.$token-space-400}; --osui-tooltip-font-size: #{variables.$token-font-size-350}; - --osui-tooltip-shadow: #{variables.$token-elevation-2}; - --osui-tooltip-max-width: 184px; + --osui-tooltip-shadow: #{variables.$token-elevation-3}; + --osui-tooltip-min-height: #{variables.$token-scale-900}; + --osui-tooltip-max-width: #{variables.$token-scale-5000}; // ─────────────────────────────────────────────────────────────────── --osui-tooltip-background-color: var(--osui-tooltip-background); @@ -37,6 +38,7 @@ box-shadow: var(--osui-tooltip-shadow); font-weight: variables.$token-font-weight-regular; line-height: initial; + min-height: var(--osui-tooltip-min-height); max-width: var(--osui-tooltip-max-width); padding: var(--osui-tooltip-padding); diff --git a/src/scss/07-keyframes/_list-item.scss b/src/scss/07-keyframes/_list-item.scss deleted file mode 100644 index 1c3347fda2..0000000000 --- a/src/scss/07-keyframes/_list-item.scss +++ /dev/null @@ -1,16 +0,0 @@ -/* KeyFrames - List Item */ -//// -/// @group KeyFrames-List_Item -/// KeyFrames - List Item - -/// -@keyframes list-item-scale-animation { - from { - opacity: 0.3; - transform: scale(0, 1) translateZ(0); - } - to { - opacity: 0; - transform: scale(400, 300) translateZ(0); - } -} diff --git a/stories/Section.stories.ts b/stories/Section.stories.ts index 158c090138..0966608dfb 100644 --- a/stories/Section.stories.ts +++ b/stories/Section.stories.ts @@ -62,7 +62,7 @@ export const Group: StoryObj = { }, render: ({ isSticky, extendedClass }) => renderStatic(` -
+
${groupSection('Profile', 6)} ${groupSection('Notifications', 6)} diff --git a/stories/Tag.stories.ts b/stories/Tag.stories.ts index 493096b3fd..c4cf9f9267 100644 --- a/stories/Tag.stories.ts +++ b/stories/Tag.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/html-vite'; import { renderStatic } from './_helpers/osui'; -import { cls, COLOR_OPTIONS, extendedClassArgType } from './_helpers/lowcode'; +import { cls, COLOR_OPTIONS_FULL, extendedClassArgType } from './_helpers/lowcode'; /** * Tag — low-code input parameters from the library OML wired as Storybook controls. @@ -10,6 +10,9 @@ import { cls, COLOR_OPTIONS, extendedClassArgType } from './_helpers/lowcode'; * Size → tag-small | tag-medium (no class = default compact size) * Shape → border-radius-none | border-radius-soft | border-radius-rounded | border-radius-circle * IsLight → appends -lightest for palette colors; -light for semantic colors (success/warning/error/info) + * + * The pattern SCSS now owns the IsLight text colors (per-family text tokens per the + * Figma spec); the text-{color} classes below are kept only for platform markup parity. */ const meta: Meta = { title: 'Patterns/Content/Tag' }; export default meta; @@ -46,7 +49,7 @@ export const Default: StoryObj = { color: { name: 'Color', control: 'select', - options: COLOR_OPTIONS, + options: COLOR_OPTIONS_FULL, description: 'Background color of the Block.', }, size: { diff --git a/stories/Tooltip.stories.ts b/stories/Tooltip.stories.ts index be53fdbf34..7920206fda 100644 --- a/stories/Tooltip.stories.ts +++ b/stories/Tooltip.stories.ts @@ -12,10 +12,26 @@ const meta: Meta = { tags: ['!ui-pending', 'ui-reviewed'], argTypes: { isHover: { control: 'boolean', name: 'IsHover' }, - position: { control: 'select', options: ['Top', 'Bottom', 'Left', 'Right'], name: 'Position' }, + position: { + control: 'select', + // GlobalEnum.Position values — TooltipConfig.validateInRange silently + // falls back to 'right' for anything outside this list. + options: [ + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + 'left', + 'right', + 'center', + ], + name: 'Position', + }, startVisible: { control: 'boolean', name: 'StartVisible' }, }, - args: { isHover: true, position: 'Bottom', startVisible: false }, + args: { isHover: true, position: 'bottom', startVisible: false }, }; export default meta; @@ -24,28 +40,45 @@ type Story = StoryObj; export const Default: Story = { render: (args) => { const id = uid('tooltip'); + const id2 = uid('tooltip'); const template = ` -
-
-
- Hover me +
+
+
+
+ +
+
- `; +
+ `; return renderPattern(template, (_root, register) => { const P = Patterns(); - P.TooltipAPI.Create( - id, - cfg({ IsHover: args.isHover, Position: args.position, StartVisible: args.startVisible }) - ); - P.TooltipAPI.Initialize(id); - register(() => P.TooltipAPI.Dispose?.(id)); + [id, id2].forEach((tooltipId) => { + P.TooltipAPI.Create( + tooltipId, + cfg({ IsHover: args.isHover, Position: args.position, StartVisible: args.startVisible }) + ); + P.TooltipAPI.Initialize(tooltipId); + register(() => P.TooltipAPI.Dispose?.(tooltipId)); + }); }); }, }; diff --git a/stories/UserAvatar.stories.ts b/stories/UserAvatar.stories.ts index 6388ed43d7..93e386eac2 100644 --- a/stories/UserAvatar.stories.ts +++ b/stories/UserAvatar.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/html-vite'; import { renderStatic } from './_helpers/osui'; -import { cls, COLOR_OPTIONS, extendedClassArgType } from './_helpers/lowcode'; +import { cls, COLOR_OPTIONS_FULL, extendedClassArgType } from './_helpers/lowcode'; /** * UserAvatar — low-code input parameters from the library OML wired as Storybook controls. @@ -69,7 +69,7 @@ export const Default: StoryObj = { color: { name: 'Color', control: 'select', - options: COLOR_OPTIONS, + options: COLOR_OPTIONS_FULL, description: 'Background color of the Block.', }, size: { @@ -100,8 +100,9 @@ export const Default: StoryObj = { const shapeClass = shape ? `border-radius-${shape}` : ''; const sizeClass = size ? `avatar-${size}` : ''; const label = `user initials, ${initials(name)}`; + // Image sizing/fit/radius is owned by the pattern SCSS (Type=Image in Figma). const inner = image - ? `${name}` + ? `${name}` : `${initials(name)}`; return renderStatic( `` diff --git a/stories/_helpers/lowcode.ts b/stories/_helpers/lowcode.ts index 9a0d362c6c..bfad9b50e2 100644 --- a/stories/_helpers/lowcode.ts +++ b/stories/_helpers/lowcode.ts @@ -33,6 +33,37 @@ export const COLOR_OPTIONS = [ 'cyan', ]; +const PALETTE_FAMILIES = [ + 'red', + 'orange', + 'yellow', + 'lime', + 'green', + 'teal', + 'cyan', + 'blue', + 'indigo', + 'violet', + 'grape', + 'pink', +]; + +/** + * The FULL `Color` set → `background-{value}` utility class: brand, + * transparent, neutral-0..10, and the 12 extended palette families (base + * shade). '' = none. Shade variants (`-lightest`…`-darkest`) and the semantic + * colors (error/warning/success/info) are left to the IsLight toggle / + * ExtendedClass. + */ +export const COLOR_OPTIONS_FULL = [ + '', + 'primary', + 'secondary', + 'transparent', + ...Array.from({ length: 11 }, (_, i) => `neutral-${i}`), + ...PALETTE_FAMILIES, +]; + /** Shared `ExtendedClass` control — present on essentially every OUI block. */ export const extendedClassArgType = { name: 'ExtendedClass', diff --git a/stories/widgets/List.stories.ts b/stories/widgets/List.stories.ts index 1a0260bbb5..211f12248f 100644 --- a/stories/widgets/List.stories.ts +++ b/stories/widgets/List.stories.ts @@ -13,21 +13,47 @@ import { renderStatic } from '../_helpers/osui'; * `.list.list-group` > `[data-list-item].list-item(.list-item-selected)` * > `.list-item-content` > `-left | -center(-title/-text) | -right`. * - * Hover and selected styling key off the `.desktop` body class set by the - * Storybook preview decorator. + * Each row composes the full Figma layout: a UserAvatar in the left region, the + * title/text in the center, and a trailing caret in the right region. The + * caret also exercises `--osui-list-item-selected-icon-color`, which recolors + * it on the selected row. + * + * Hover, pressed and selected styling key off the `.desktop` body class set by + * the Storybook preview decorator. */ const meta: Meta = { title: 'Widgets/List' }; export default meta; type Story = StoryObj; -const item = (selected: boolean, title: string, text: string, right = ''): string => ` +/** Derive initials: first letter of each word, up to 2 characters, uppercased. */ +function initials(name: string): string { + return name + .trim() + .split(/\s+/) + .map((w) => w[0] ?? '') + .join('') + .slice(0, 2) + .toUpperCase(); +} + +// Matches the Figma List Item spec: 40px (avatar-medium) circular indigo avatar. +const avatar = (title: string): string => ` + `; + +const caretRight = ``; + +const item = (selected: boolean, title: string, text: string): string => `
+
${avatar(title)}
${title}
${text}
- ${right ? `
${right}
` : ''} +
${caretRight}
`; @@ -36,7 +62,7 @@ export const Default: Story = { renderStatic(`
${item(false, 'First item', 'A tappable row with a title and supporting text.')} - ${item(true, 'Selected item', 'This row is selected.', '')} + ${item(true, 'Selected item', 'This row is selected.')} ${item(false, 'Third item', 'Another row.')}
`), };