Составьте локальный план обучения с составным компонентом FormField, управляемым TextInput и PrimaryButton из Canvas Kit. При этом различайте актуальные источники семантических токенов и старые примеры миграции с space.x4.
Оригинальная автономная интерпретация HTML паттернов взаимодействия Canvas и сдержанной корпоративной иерархии. Палитра и макет выбраны для приложения и не являются отображением тенанта Sana. · Справочник по реализации
02
Справочник по дизайн-системе Workday Canvas
Текущие исходный код Canvas Kit и файл токенов Canvas sys.json. Приведённые ниже имена в формате JSON — это пути в исходном коде, а не придуманные имена переменных CSS. Значения, ссылающиеся на базовые токены, показаны как псевдонимы; справочник по миграции с указанием даты явно обозначен как исторический.
Дизайн-система Workday Canvas: изучите текущий исходный код, прежде чем копировать пример миграции
Теперь интервалы различаются по назначению
Система Canvas развивалась от констант интервалов в формате «размер футболки» в canvas-kit-react/tokens через пронумерованные токены system.space до категорий исходного кода, таких как отступы и интервалы. В актуальном sys.json их базовые значения заданы ссылками на базовые токены. В этом справочнике приведены фактические пути, а не выдуманные имена переменных CSS, составленные простым объединением слов.
Таблица миграции по-прежнему пригодится при обновлении старого приложения: в ней объясняется, почему прежнее значение space.s соответствовало одному rem. Это исторический источник, а не гарантия, что каждый старый экспорт и сейчас является предпочтительным интерфейсом. В предварительном просмотре используются интервалы приложения 16 и 24px; таблица исходного кода остаётся главным источником информации об именах токенов библиотеки.
Дизайн-система Workday Canvas: цвет зависит от тенанта и состояния компонента
Основная кнопка — это не просто неизменный синий образец
Сначала PrimaryButton обращается к brand.action.base, а при его отсутствии использует устаревший основной акцентный цвет. При наведении и нажатии также проверяются роли действий brand, прежде чем состояния формируются с помощью вспомогательных функций colorSpace. Поэтому замена одного жёстко заданного цвета фона не позволит воспроизвести тему компонента.
В исходном примере учебного плана используется сдержанная синяя палитра приложения, и он обозначен как интерпретация HTML. Это не отображение темы конкретного клиентского тенанта и не новой темы Sana Canvas. В полном нативном примере цвета компонентов задаются загруженными слоями токенов.
Дизайн-система Workday Canvas: составные роли сохраняют межстрочный интервал и насыщенность
Псевдоним размера — это не полный стиль
Объект типографики body.sm в текущем исходном коде объединяет семейство шрифтов, обычную насыщенность, размер основного текста и межстрочный интервал. Для заголовков используются роли с жирной насыщенностью. Если взять только размер в пикселях со старого скриншота, эти взаимосвязи потеряются. В исходном коде используется ключ normal, а не regular, встречающийся в прежних материалах по миграции.
Пакет компонентов Canvas Kit не включает шрифты. Загрузку шрифтов следует рассматривать как отдельное решение для интеграции; в автономном примере используются системные запасные шрифты, и он не подразумевает, что Roboto или шрифт Sana входят в комплект.
Дизайн-система Workday Canvas: FormField — составной компонент
Соберите метку, поле ввода и подсказку внутри поля
В исходном коде доступны FormField.Label, FormField.Input и FormField.Hint. Передача TextInput в слот Input через проп as позволяет модели сформировать доступные связи между элементами. Это отличается от старых примеров, в которых метка задавалась пропом label внешнего поля, а внутрь напрямую помещалось поле ввода без обёртки.
Для горизонтальных форм актуальны значения ориентации horizontalStart и horizontalEnd. FormField.Field группирует поле ввода и подсказку в рамках этого выравнивания. Наш фильтр остаётся вертикальным, поскольку пояснение должно располагаться непосредственно под меткой и полем ввода.
Поместите видимую метку в FormField.Label.
Передайте TextInput через составной слот Input.
Используйте Hint, чтобы указать область действия фильтра.
При добавлении ограничений оставьте логику проверки в приложении.
Дизайн-система Workday Canvas: для растущего поля нужен гибкий родительский контейнер
Учитывайте минимальную ширину
У TextInput задана минимальная ширина 280px по умолчанию, выраженная в исходном коде через px2rem. Параметр grow запрашивает полную ширину, но не позволяет жёсткому макету из двух столбцов уместиться в узкой области просмотра. В автономном каталоге на мобильных устройствах боковая панель плана размещается под карточками курсов, а каждый столбец макета можно сжать.
Поисковый запрос хранится в обычном состоянии React. Приложение выполняет поиск подстроки без учёта регистра по трём названиям курсов и явно показывает пустой результат. Фильтр не очищает выбранные курсы, поскольку видимость и участие в плане — разные состояния.
Дизайн-система Workday Canvas: выбор сохраняется независимо от видимых результатов
Используйте постоянные ID курсов и вычисляйте итоговые значения
У каждого исходного курса есть ID, название и продолжительность. В списке выбранного хранятся ID, а отображаемая общая продолжительность вычисляется по записям курсов. Фильтрация каталога не удаляет выбранный элемент. После удаления элемента действие «Добавить» снова становится доступно, а общее время уменьшается.
Это оригинальный локальный сценарий планирования. Он не вызывает API Workday, не записывает сотрудника на курс и не изменяет учебную запись. Ясная граница возможностей не позволяет прототипу создавать впечатление, будто он выполняет реальную деловую операцию.
Фильтруйте только каталог.
Добавляйте каждый ID курса не более одного раза.
Вычисляйте выбранные курсы и общее количество минут по этим ID.
Дизайн-система Workday Canvas: загрузите токены, прежде чем ожидать, что компоненты подстроятся под тему
У провайдера и слоёв CSS разные задачи
В текущем примере для начала работы стили переменных base, brand, component и system импортируются в точке входа приложения, после чего интерфейс оборачивается в CanvasProvider. Один только провайдер не заменяет эти определения CSS. Приведённый здесь код следует этой структуре и задаёт полный корневой элемент React.
Sana Canvas подключается по желанию: его переменные загружаются после системных переменных, а для выбора темы используется документированный атрибут html data-theme. Для встроенных приложений, которые не могут управлять корневым элементом документа, предусмотрен отдельный параметр темы провайдера для передачи темы всплывающим окнам. При обновлении пакетов не добавляйте эту тему случайно в существующий тенант.
Сохраните весь документ как index.html и откройте его в браузере. Оригинальная автономная интерпретация HTML паттернов взаимодействия Canvas и сдержанной корпоративной иерархии. Палитра и макет выбраны для приложения и не являются отображением тенанта Sana.
html
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Canvas learning plan interpretation</title><style>*{box-sizing:border-box}body{margin:0;background:#f3f5f7;color:#20272f;font:16px/1.5 system-ui,sans-serif}button,input{font:inherit}header{background:white;border-bottom:1px solid #d5dbe0;padding:16px 24px;display:flex;gap:12px;align-items:center}.mark{width:32px;height:32px;border:4px solid #ed9b28;border-bottom:0;border-radius:50% 50% 0 0;height:16px;margin-right:8px}header span{font-size:14px;color:#59616b;margin-left:auto}main{max-width:1100px;margin:32px auto;padding:0 24px}h1{font-size:28px;line-height:36px;margin:0 0 8px}p{margin:0 0 24px}label{display:block;font-size:14px;font-weight:600;margin-bottom:8px}#search{width:100%;max-width:580px;height:40px;border:1px solid #89949f;border-radius:8px;padding:8px 12px;background:white}.hint{font-size:12px;color:#59616b;margin:8px 0 24px}.layout{display:grid;grid-template-columns:1fr 290px;gap:24px;align-items:start}.course{display:grid;grid-template-columns:96px 1fr;gap:24px;background:white;padding:24px;border-radius:8px;border:1px solid #d5dbe0;margin-bottom:16px;min-width:0}.art{height:96px;display:grid;place-items:center;border-radius:8px;font-size:40px;color:#005cb9;background:#e7f1fd}.art.orange{background:#fff0d6;color:#805200}.art.green{background:#e6f4ed;color:#236443}.category{font-size:12px;color:#59616b;text-transform:uppercase;letter-spacing:1px}h2{font-size:20px;line-height:28px;margin:4px 0 8px}.course p{font-size:14px;margin:0 0 16px;color:#59616b}.row{display:flex;justify-content:space-between;align-items:center;gap:16px}.row span{font-size:14px;color:#59616b}button{border:0;border-radius:999px;background:#005cb9;color:white;font-size:14px;font-weight:600;padding:8px 16px;cursor:pointer}button:disabled{opacity:.5;cursor:default}button:focus-visible,input:focus-visible{outline:3px solid #005cb9;outline-offset:3px}.plan{padding:24px;background:white;border:1px solid #d5dbe0;border-radius:8px}.plan h2{margin:0 0 16px}.number{font-size:36px;font-weight:700;line-height:1.2}.plan-total{padding-bottom:20px;border-bottom:1px solid #d5dbe0;margin-bottom:20px}.plan-total span{font-size:14px}.plan ul{padding:0;list-style:none;margin:0}.plan li{border-bottom:1px solid #e6e9ed;padding:0 0 16px;margin:0 0 16px;font-size:14px}.remove{background:transparent;color:#005cb9;padding:4px 0;display:block;border-radius:3px;text-decoration:underline;font-weight:400}.empty{font-size:14px;color:#59616b}.boundary{font-size:12px;color:#59616b;margin-top:24px}.no-results{background:white;border:1px solid #d5dbe0;padding:24px;border-radius:8px}@media(max-width:720px){main{padding:0 16px;margin:24px auto}.layout{grid-template-columns:1fr}.course{padding:16px;grid-template-columns:60px 1fr;gap:16px}.art{height:60px;font-size:28px}header span{display:none}.row{align-items:flex-start;flex-direction:column;gap:8px}.plan{padding:20px}}
</style><header><div class="mark" aria-hidden="true"></div><strong>Learning workspace</strong><span>Personal development · Local plan</span></header><main><h1>Make room to grow</h1><p>Choose a few focused lessons for your learning plan.</p><label for="search">Find a course</label><input id="search" type="search" placeholder="Search course titles" aria-describedby="hint"><p id="hint" class="hint">Search filters the catalog; your selected courses stay in the plan.</p><div class="layout"><section id="catalog" aria-label="Course catalog"></section><aside class="plan"><h2>Your local plan</h2><div class="plan-total" role="status"><div class="number" id="minutes">0</div><span>minutes · <span id="count">0</span> <span id="course-word">courses</span></span></div><ul id="selected"></ul><p class="empty" id="empty">Add a course to start your plan.</p><small>No enrollment is submitted.</small></aside></div><p class="boundary">Independent HTML interpretation of Canvas composition patterns. Original palette, course illustrations and content; no Workday account or enrollment service is connected.</p></main><script>const courses=[{id:'feedback',name:'Giving useful feedback',minutes:25,category:'Communication',description:'Practice clear observations and constructive next steps.',art:'◎',color:''},{id:'planning',name:'Planning a focused week',minutes:20,category:'Personal effectiveness',description:'Protect attention and organize a realistic working week.',art:'▤',color:'orange'},{id:'meetings',name:'Running inclusive meetings',minutes:30,category:'Teamwork',description:'Make space for different voices and useful decisions.',art:'◌',color:'green'}];let chosen=new Set;const catalog=document.querySelector('#catalog');function render(){let matches=courses.filter(c=>c.name.toLowerCase().includes(document.querySelector('#search').value.toLowerCase()));catalog.innerHTML=matches.length?matches.map(c=>`<article class="course"><div class="art ${c.color}" aria-hidden="true">${c.art}</div><div><div class="category">${c.category}</div><h2>${c.name}</h2><p>${c.description}</p><div class="row"><span>${c.minutes} minutes</span><button data-add="${c.id}" ${chosen.has(c.id)?'disabled':''}>${chosen.has(c.id)?'Added to plan':'Add to plan'}</button></div></div></article>`).join(''):'<div class="no-results">No matching courses. Try “feedback” or clear your search.</div>';let selected=courses.filter(c=>chosen.has(c.id));document.querySelector('#minutes').textContent=selected.reduce((sum,c)=>sum+c.minutes,0);document.querySelector('#count').textContent=selected.length;document.querySelector('#course-word').textContent=selected.length===1?'course':'courses';document.querySelector('#empty').hidden=selected.length>0;document.querySelector('#selected').innerHTML=selected.map(c=>`<li>${c.name}<button class="remove" data-remove="${c.id}" aria-label="Remove ${c.name}">Remove</button></li>`).join('');document.querySelectorAll('[data-add]').forEach(b=>b.onclick=()=>{let id=b.dataset.add;chosen.add(id);render();document.querySelector('[data-remove="'+id+'"]').focus()});document.querySelectorAll('[data-remove]').forEach(b=>b.onclick=()=>{let id=b.dataset.remove;chosen.delete(id);render();let add=document.querySelector('[data-add="'+id+'"]');(add||document.querySelector('#search')).focus()})}document.querySelector('#search').oninput=render;render();</script></html>
Добавление курса изменяет пример учебного плана. В отдельной нативной записи демонстрируется использование CanvasProvider и составного компонента FormField API; запись на курс не отправляется.
Составное поле и выбираемые учебные элементы
FormField создаёт связь с полем ввода, TextInput фильтрует исходные данные курсов, а каждая PrimaryButton добавляет элемент не более одного раза. При удалении общее время выбранных курсов пересчитывается.
jsx
// In a React 18+ application with a bundler:
// npm install @workday/canvas-kit-react @workday/canvas-tokens-web @workday/canvas-kit-styling @emotion/react @emotion/styled
import React,{useState} from 'react';
import {createRoot} from 'react-dom/client';
import {CanvasProvider} from '@workday/canvas-kit-react/common';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {TextInput} from '@workday/canvas-kit-react/text-input';
import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {system} from '@workday/canvas-tokens-web';
import {cssVar} from '@workday/canvas-kit-styling';
import '@workday/canvas-tokens-web/css/base/_variables.css';
import '@workday/canvas-tokens-web/css/brand/_variables.css';
import '@workday/canvas-tokens-web/css/component/_variables.css';
import '@workday/canvas-tokens-web/css/system/_variables.css';
// Default theme example; do not silently add a Sana theme to an existing tenant.
const courses=[{id:'feedback',name:'Giving useful feedback',minutes:25},{id:'planning',name:'Planning a focused week',minutes:20},{id:'meetings',name:'Running inclusive meetings',minutes:30}];
function LearningPlan(){
const [query,setQuery]=useState(''),[chosen,setChosen]=useState([]);
const visible=courses.filter(c=>c.name.toLowerCase().includes(query.toLowerCase()));
const selected=courses.filter(c=>chosen.includes(c.id));
return <main style={{fontFamily:cssVar(system.fontFamily.default),maxWidth:900,margin:'auto',padding:24}}>
<h1>Build a learning plan</h1>
<FormField><FormField.Label>Find a course</FormField.Label>
<FormField.Input as={TextInput} grow value={query} onChange={e=>setQuery(e.target.value)}/>
<FormField.Hint>Filter the three example course titles.</FormField.Hint>
</FormField>
<ul>{visible.map(c=><li key={c.id} style={{margin:'24px 0'}}><h2>{c.name}</h2><p>{c.minutes} minutes</p>
<PrimaryButton type="button" disabled={chosen.includes(c.id)} onClick={()=>setChosen(old=>[...old,c.id])}>
{chosen.includes(c.id)?'Added to plan':'Add to plan'}
</PrimaryButton></li>)}</ul>
{!visible.length&&<p>No matching courses.</p>}
<section aria-label="Learning plan"><h2>Your local plan</h2><p role="status">{selected.length} courses · {selected.reduce((sum,c)=>sum+c.minutes,0)} minutes</p>
<ul>{selected.map(c=><li key={c.id}>{c.name} <button type="button" onClick={()=>setChosen(old=>old.filter(id=>id!==c.id))}>Remove {c.name}</button></li>)}</ul>
<p>No enrollment is submitted.</p></section></main>;
}
createRoot(document.getElementById('root')).render(<CanvasProvider><LearningPlan/></CanvasProvider>);
PrimaryButton относится к отдельному курсу; для всего плана нет вводящего в заблуждение действия записи на курс.
Для удаления используется обычная кнопка, явно принадлежащая приложению.
Недоступное состояние «Добавлено» предотвращает повторное добавление, а счётчик подтверждает изменение.
04
Настройка AI-агента для дизайн-системы Workday Canvas
Отправная точка для реализации
Полная структура React для собранного приложения. Актуальная настройка CanvasProvider и слоёв токенов CSS; исходные данные курсов и нативные элементы управления для удаления. Удалённая запись на курс не выполняется. Исходный код проверен, но в этой рабочей среде не компилировался с пакетом.
jsx
// In a React 18+ application with a bundler:
// npm install @workday/canvas-kit-react @workday/canvas-tokens-web @workday/canvas-kit-styling @emotion/react @emotion/styled
import React,{useState} from 'react';
import {createRoot} from 'react-dom/client';
import {CanvasProvider} from '@workday/canvas-kit-react/common';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {TextInput} from '@workday/canvas-kit-react/text-input';
import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {system} from '@workday/canvas-tokens-web';
import {cssVar} from '@workday/canvas-kit-styling';
import '@workday/canvas-tokens-web/css/base/_variables.css';
import '@workday/canvas-tokens-web/css/brand/_variables.css';
import '@workday/canvas-tokens-web/css/component/_variables.css';
import '@workday/canvas-tokens-web/css/system/_variables.css';
// Default theme example; do not silently add a Sana theme to an existing tenant.
const courses=[{id:'feedback',name:'Giving useful feedback',minutes:25},{id:'planning',name:'Planning a focused week',minutes:20},{id:'meetings',name:'Running inclusive meetings',minutes:30}];
function LearningPlan(){
const [query,setQuery]=useState(''),[chosen,setChosen]=useState([]);
const visible=courses.filter(c=>c.name.toLowerCase().includes(query.toLowerCase()));
const selected=courses.filter(c=>chosen.includes(c.id));
return <main style={{fontFamily:cssVar(system.fontFamily.default),maxWidth:900,margin:'auto',padding:24}}>
<h1>Build a learning plan</h1>
<FormField><FormField.Label>Find a course</FormField.Label>
<FormField.Input as={TextInput} grow value={query} onChange={e=>setQuery(e.target.value)}/>
<FormField.Hint>Filter the three example course titles.</FormField.Hint>
</FormField>
<ul>{visible.map(c=><li key={c.id} style={{margin:'24px 0'}}><h2>{c.name}</h2><p>{c.minutes} minutes</p>
<PrimaryButton type="button" disabled={chosen.includes(c.id)} onClick={()=>setChosen(old=>[...old,c.id])}>
{chosen.includes(c.id)?'Added to plan':'Add to plan'}
</PrimaryButton></li>)}</ul>
{!visible.length&&<p>No matching courses.</p>}
<section aria-label="Learning plan"><h2>Your local plan</h2><p role="status">{selected.length} courses · {selected.reduce((sum,c)=>sum+c.minutes,0)} minutes</p>
<ul>{selected.map(c=><li key={c.id}>{c.name} <button type="button" onClick={()=>setChosen(old=>old.filter(id=>id!==c.id))}>Remove {c.name}</button></li>)}</ul>
<p>No enrollment is submitted.</p></section></main>;
}
createRoot(document.getElementById('root')).render(<CanvasProvider><LearningPlan/></CanvasProvider>);
Запрос для вашего проекта
Промпт для AI-агента
Используйте текущую композицию Canvas FormField и слои токенов CSS. Отделите фильтрацию курсов от состояния выбора, предотвращайте дубликаты и рассчитывайте общее количество минут. Сохраняйте темизацию арендатора через семантические роли; не принимайте исторические названия пространства с номерами за актуальные предпочтительные категории источников.
05
Источники и область применения дизайн-системы Workday Canvas
Самостоятельный справочник OpenDesign. Зачисление сотрудников и Workday API не используются. Актуальные названия источников явно отделены от прежних названий для миграции.