Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@ All notable changes to `@sema-lang/ui` are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/), and the project adheres to
semantic versioning.

## [0.2.0]

### Added

- **`sema-editor`: `blur()` delegate.** Mirrors the existing `focus()` delegate,
blurring the inner shadow textarea. Added so consumers that programmatically
focus the editor (e.g. a notebook cell) can also programmatically blur it —
restoring behavior the pre-migration light-DOM `<textarea>` had for free.
- **`sema-editor`: honors the native `autofocus` attribute.** `firstUpdated()`
now calls `focus()` when the host carries `autofocus`, delegating into the
inner textarea (a Lit `@property` can't be named `autofocus` — it collides
with the HTMLElement IDL attribute).
- **`sema-editor`: gutter-line state exposed as part tokens.** A gutter line's
`part` attribute now carries `breakpoint` and/or `current` alongside
`gutter-line`, so consumers and e2e suites can target breakpoint/current-line
state through the shadow boundary (`::part(gutter-line breakpoint)`,
`[part~="breakpoint"]`) instead of coupling to internal class names.
- **`sema-editor`: documented keydown contract.** The class doc now states that
the inner textarea's `keydown` bubbles composed to the host as public API, and
that the component intercepts only Tab — it never calls `stopPropagation()` on
any other key.
- **`sema-input` / `sema-textarea`: `testid` property.** Forwards onto the inner
control as `data-testid`, matching the existing `sema-editor` / `sema-markdown`
pattern, so shadow-piercing test queries (e.g. Playwright `getByTestId`) work.
- **`sema-button`: `variant="run"` + `danger` now reads as destructive at rest.**
A run button in a danger state (e.g. "Stop" while running) represents a
destructive action, so it now shows `--error` border + text at rest, not only
on hover (unlike the hover-only danger modifier on `debug`/`action`).

### Changed

- **BREAKING: `sema-dialog` events renamed** `sema-dialog-open` → `sema-open`,
`sema-dialog-close` → `sema-close`, aligning with `sema-popover`'s existing
event names. Consumers listening for the old event names must update to the
new ones.

### Fixed

- **`sema-dialog`: the open host has a real box.** The host's only child (the
backdrop) is `position: fixed` and out of flow, so the open host used to
compute 0×0 — visibility checks (a11y tooling, Playwright `toBeVisible`)
reported a shown dialog as hidden. `:host([open])` is now itself
`position: fixed; inset: 0`, matching what is visually painted.
- **`sema-popover`: panel no longer clipped by `overflow` ancestors.** The panel
is now `position: fixed` instead of `position: absolute`, measured against the
trigger's `getBoundingClientRect()` on open, flipped vertically when it
would cross the viewport edge, and clamped horizontally so an edge-adjacent
trigger can't push it off-viewport. Because the resulting coordinates are only
valid until the page scrolls or resizes, the popover now closes on either
while open (scrolling inside the popover's own content is exempt).

## [0.1.3]

### Added
Expand Down
18 changes: 8 additions & 10 deletions TODO-COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ admin chrome) were dropped — they belong in their app, not the library.
| Toast | `sema-toast`, `sema-toaster`, `toast()` | (#17) imperative API + singleton region; variants, stacking, auto-dismiss + pause-on-hover, `dismiss`/`dismissAll` |
| Layout primitives | `sema-container`, `sema-grid`, `sema-sidebar` | (L1–L3) CSS-first, intrinsic responsiveness (no breakpoint props); **`sema-split` renamed → `sema-sidebar`** to avoid confusion with the interactive `sema-splitter` |
| Tabs | `sema-tabs`, `sema-tab`, `sema-tab-panel` | (#4) panel tabs only — segmented control already shipped as `sema-toggle-group`. Light-DOM ARIA (IDREFs resolve), auto/`manual` activation, `hidden="until-found"` find-in-page, opt-in `hash-sync` deep links |
| Badge / Tag | `sema-badge` | (#9) variants: neutral/gold/success/error, `pill` shape + `dot` — **absorbs Provider Pill List #23** |
| Drawer / Slide Panel | `sema-drawer` | (#16) generic & dockable: left/right/top/bottom, focus trap, scroll lock |
| Spinner / Loading | `sema-spinner` | (#2) minimal inline indicator, `role="status"` live region |
| Pagination | `sema-pagination` | (#19) |
| Shortcut / KBD | `sema-kbd` | (#27) single key or `+`-separated combo |

---

## 🔜 Next — priority (ordered)

1. Badge / Tag — `sema-badge` (#9; **absorbs Provider Pill List #23** as a pill variant in a wrap)
2. Drawer / Slide Panel — `sema-drawer` (#16) — **generic & dockable: left / right / top / bottom**, not scoped to the registry admin use-case
3. Spinner / Loading — `sema-spinner` (#2) — **minimal**: inline indicator (size + `role=status`/`aria-label`). Drop the fullscreen "boot splash" mode — that's app composition, not a primitive.
4. Data Table — `sema-table` (#18). Larger build; requirements: **sortable headers** (sort-direction indicator + `aria-sort`), **hoverable rows**, **row/cell click events**, **mobile-responsive horizontal overflow** (`overflow-x:auto` + `overscroll-behavior-x: contain` + momentum scroll).
5. Pagination — `sema-pagination` (#19)
6. Shortcut / KBD — `sema-kbd` (#27)
1. Data Table — `sema-table` (#18). Larger build; requirements: **sortable headers** (sort-direction indicator + `aria-sort`), **hoverable rows**, **row/cell click events**, **mobile-responsive horizontal overflow** (`overflow-x:auto` + `overscroll-behavior-x: contain` + momentum scroll).

---

Expand All @@ -62,10 +62,8 @@ admin chrome) were dropped — they belong in their app, not the library.

## ⏸ Deferred fixes (from the 2026-06 quality sweep)

- **Packaging/publish portability** — dist + `.d.ts`, exports map, externalized-Lit build, define guards. Latent behind `private: true`; do as one batch when publishing is planned.
- **Event-name consistency** — dialog `sema-dialog-open/close` vs popover `sema-open/close`. Breaking; defer to a deliberate release.
- **CI Node job** — blocked: `brand/ui/` is untracked in git; commit `brand/` first, then add npm ci + playwright + lint + test to ci.yml.
- **Popover positioning** — top-layer/portal for overflow-clipping containers; JS measure-and-flip. Interim: use `native` selects inside scroll containers.
- **Event-name consistency** — dialog `sema-dialog-open/close` vs popover `sema-open/close`. **Resolved in 0.2.0**: dialog now emits `sema-open`/`sema-close` (breaking change, see CHANGELOG).
- **Popover positioning** — top-layer/portal for overflow-clipping containers; JS measure-and-flip. **Resolved in 0.2.0**: `<sema-popover>`'s panel is `position: fixed`, measured against the trigger on open (with vertical flip near a viewport edge), so it escapes `overflow`-clipping ancestors.
- **Test `waitFor` consolidation** — five duplicated helpers → `tests/_util.ts`; replace `setTimeout(150)` negative assertions in sema-code tests with a `highlightComplete` signal.
- Menu type-ahead (APG SHOULD) · sema-toggle `selected`-is-group-managed JSDoc note.
- Small website primitives: `sema-cta-group` (#43) · `sema-diagram-frame` (#46) · `sema-status-cell` (#48) · `sema-arrow-link` (#49, → Button variant) · `sema-text-divider` (#56).
Expand Down
60 changes: 55 additions & 5 deletions custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -1155,7 +1155,7 @@
"declarations": [
{
"kind": "class",
"description": "",
"description": "`<sema-dialog>` — a modal dialog with a focus trap, backdrop click, Escape\nclose, scroll lock, and header/body/footer slots.\n\nEmits `sema-open` / `sema-close` (aligned with `<sema-popover>`).",
"name": "SemaDialog",
"members": [
{
Expand Down Expand Up @@ -1233,13 +1233,13 @@
],
"events": [
{
"name": "sema-dialog-open",
"name": "sema-open",
"type": {
"text": "CustomEvent"
}
},
{
"name": "sema-dialog-close",
"name": "sema-close",
"type": {
"text": "CustomEvent"
}
Expand Down Expand Up @@ -1486,7 +1486,7 @@
"declarations": [
{
"kind": "class",
"description": "`<sema-editor>` — the single editable, syntax-highlighting code editor for the\nSema ecosystem (notebook cells, the playground, anywhere code is edited).\n\nA transparent `<textarea>` (real caret / selection / IME / undo) sits over an\n`aria-hidden` overlay of per-line `<div>`s painted by the **one** shared Shiki\nhighlighter — so it renders identically to `<sema-code>` and works for every\nlanguage, not just Sema. Highlighting is synchronous after a one-time async\ngrammar warmup; until warm it shows escaped plain text and upgrades in place.\n\n## Gutter (line numbers + breakpoints + debug line)\nOpt in with `line-numbers`. The gutter is a *mechanism*: it renders the markers it\nis told about and reports clicks — it owns no policy. Consumers drive it:\n- `.breakpoints = [3, 7]` — lines that show a breakpoint dot.\n- `current-line=\"5\"` — the active/debug line (highlighted in gutter + editor).\n- listen for `gutter-click` (`detail: { line }`) to toggle/snap breakpoints, etc.\n\nEvents: `input` and `change` as `CustomEvent<{ value }>`; native `keydown` bubbles\n(composed) so hosts can bind Shift+Enter etc.",
"description": "`<sema-editor>` — the single editable, syntax-highlighting code editor for the\nSema ecosystem (notebook cells, the playground, anywhere code is edited).\n\nA transparent `<textarea>` (real caret / selection / IME / undo) sits over an\n`aria-hidden` overlay of per-line `<div>`s painted by the **one** shared Shiki\nhighlighter — so it renders identically to `<sema-code>` and works for every\nlanguage, not just Sema. Highlighting is synchronous after a one-time async\ngrammar warmup; until warm it shows escaped plain text and upgrades in place.\n\n## Gutter (line numbers + breakpoints + debug line)\nOpt in with `line-numbers`. The gutter is a *mechanism*: it renders the markers it\nis told about and reports clicks — it owns no policy. Consumers drive it:\n- `.breakpoints = [3, 7]` — lines that show a breakpoint dot.\n- `current-line=\"5\"` — the active/debug line (highlighted in gutter + editor).\n- listen for `gutter-click` (`detail: { line }`) to toggle/snap breakpoints, etc.\n\nEvents: `input` and `change` as `CustomEvent<{ value }>`. The inner textarea's\nnative `keydown` bubbles composed to the host — this is part of the public API,\nso hosts bind Shift+Enter, Escape, Cmd+S, etc. directly on the host element. The\ncomponent itself intercepts **only Tab** (`preventDefault()` + indent/dedent) and\nnever calls `stopPropagation()`, on Tab or any other key.",
"name": "SemaEditor",
"members": [
{
Expand Down Expand Up @@ -1633,6 +1633,11 @@
"name": "focus",
"description": "Focus delegates to the inner textarea (the host itself isn't focusable)."
},
{
"kind": "method",
"name": "blur",
"description": "Blur delegates to the inner textarea."
},
{
"kind": "method",
"name": "scrollToLine",
Expand Down Expand Up @@ -2153,6 +2158,15 @@
"description": "Maximum number of characters the control accepts (omit for no limit).",
"attribute": "maxlength"
},
{
"kind": "field",
"name": "testid",
"type": {
"text": "string"
},
"default": "''",
"attribute": "testid"
},
{
"kind": "field",
"name": "_internals",
Expand Down Expand Up @@ -2262,6 +2276,14 @@
},
"description": "Maximum number of characters the control accepts (omit for no limit).",
"fieldName": "maxlength"
},
{
"name": "testid",
"type": {
"text": "string"
},
"default": "''",
"fieldName": "testid"
}
],
"superclass": {
Expand Down Expand Up @@ -3061,7 +3083,7 @@
"declarations": [
{
"kind": "class",
"description": "`<sema-popover>` — a generic anchored popover.\n\nPut the trigger in `slot=\"trigger\"` and the floating content in the default slot.\nOpens on trigger click (or hover via `open-on=\"hover\"`).\n\n**Focus & keyboard (WAI-ARIA menu-button pattern):** opening moves focus into the\npanel (a child `<sema-menu>` focuses its first item); Escape, outside-click, Tab, and\nfocus leaving the popover all close it; Escape/Tab return focus to the trigger. The\ntrigger gets `aria-haspopup` + `aria-expanded`. Add the **`modal`** attribute for\ndialog-like content that should hard-trap Tab instead of closing on Tab/focus-out.\n\nEmits `sema-open` / `sema-close`. A child `<sema-menu>`'s `sema-select` auto-closes it.",
"description": "`<sema-popover>` — a generic anchored popover.\n\nPut the trigger in `slot=\"trigger\"` and the floating content in the default slot.\nOpens on trigger click (or hover via `open-on=\"hover\"`).\n\n**Focus & keyboard (WAI-ARIA menu-button pattern):** opening moves focus into the\npanel (a child `<sema-menu>` focuses its first item); Escape, outside-click, Tab, and\nfocus leaving the popover all close it; Escape/Tab return focus to the trigger. The\ntrigger gets `aria-haspopup` + `aria-expanded`. Add the **`modal`** attribute for\ndialog-like content that should hard-trap Tab instead of closing on Tab/focus-out.\n\n**Positioning:** the panel is `position: fixed`, so it always escapes `overflow`-\nclipping ancestors instead of being clipped by them. On open it measures the\ntrigger with `getBoundingClientRect()` and sets viewport-relative coordinates,\nflipping vertically (`bottom-*` ↔ `top-*`) when the preferred side would cross the\nviewport edge. Because the fixed coordinates go stale if the page scrolls or\nresizes, the popover closes on either while open (scrolling *inside* the popover's\nown light-DOM content is exempt, so a scrollable slotted panel doesn't self-close).\n\nEmits `sema-open` / `sema-close`. A child `<sema-menu>`'s `sema-select` auto-closes it.",
"name": "SemaPopover",
"members": [
{
Expand Down Expand Up @@ -3135,6 +3157,11 @@
"name": "_onDocPointer",
"privacy": "private"
},
{
"kind": "field",
"name": "_onViewportChange",
"privacy": "private"
},
{
"kind": "field",
"name": "_trigger",
Expand All @@ -3144,6 +3171,12 @@
"privacy": "private",
"readonly": true
},
{
"kind": "method",
"name": "_reposition",
"privacy": "private",
"description": "Measure the trigger and place the (fixed) panel at viewport-relative\ncoordinates, flipping vertically when the preferred side doesn't fit but the\nopposite side does. Called once the panel has laid out (post-`updateComplete`),\nsince it needs the panel's real dimensions."
},
{
"kind": "method",
"name": "show"
Expand Down Expand Up @@ -4806,6 +4839,15 @@
"attribute": "autosize",
"reflects": true
},
{
"kind": "field",
"name": "testid",
"type": {
"text": "string"
},
"default": "''",
"attribute": "testid"
},
{
"kind": "field",
"name": "_internals",
Expand Down Expand Up @@ -4934,6 +4976,14 @@
"default": "false",
"description": "Grow to fit content (CSS field-sizing, with a scrollHeight JS fallback).",
"fieldName": "autosize"
},
{
"name": "testid",
"type": {
"text": "string"
},
"default": "''",
"fieldName": "testid"
}
],
"superclass": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@sema-lang/ui",
"description": "Web Components for the Sema design system. Lit-based, framework-agnostic, consumes DESIGN.md tokens via CSS custom properties.",
"version": "0.1.4",
"version": "0.2.0",
"type": "module",
"license": "MIT",
"author": "Helge Sverre <helge.sverre@gmail.com>",
Expand Down
2 changes: 0 additions & 2 deletions src/lib/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ declare global {
'sema-resize': CustomEvent<SemaResizeEventDetail>;
'sema-open': Event;
'sema-close': Event;
'sema-dialog-open': Event;
'sema-dialog-close': Event;
'sema-drawer-open': Event;
'sema-drawer-close': Event;
'sema-dismiss': Event;
Expand Down
11 changes: 11 additions & 0 deletions src/lib/sema-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ export class SemaButton extends SemaElement {
outline-offset: 3px;
border-radius: 3px;
}
/* run + danger: a destructive/stop state, so it must read as danger at rest —
unlike the debug/action danger modifier below, which only tints on hover. */
:host([variant="run"][danger]) .button {
background: transparent;
border: 1px solid var(--error, #c85555);
color: var(--error, #c85555);
}
:host([variant="run"][danger]) .button:hover:not(:disabled) {
background: var(--error-bg, rgba(200, 85, 85, 0.06));
opacity: 1;
}

/* shortcut badge inside run */
.shortcut {
Expand Down
17 changes: 15 additions & 2 deletions src/lib/sema-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,28 @@ import { property } from 'lit/decorators.js';
import { SemaElement } from '../internal/sema-element.js';
import { FocusTrapController } from '../internal/controllers/focus-trap.js';

/**
* `<sema-dialog>` — a modal dialog with a focus trap, backdrop click, Escape
* close, scroll lock, and header/body/footer slots.
*
* Emits `sema-open` / `sema-close` (aligned with `<sema-popover>`).
*/
export class SemaDialog extends SemaElement {
static styles = [
SemaElement.base,
css`
:host {
display: none;
}
/* Give the open host a real box matching what is visually painted: its
only child (.backdrop) is position:fixed and out of flow, so without
this the host computes 0x0 and visibility checks (a11y tools,
Playwright toBeVisible) report the shown dialog as hidden. */
:host([open]) {
display: block;
position: fixed;
inset: 0;
z-index: 500;
}

.backdrop {
Expand Down Expand Up @@ -130,10 +143,10 @@ export class SemaDialog extends SemaElement {
if (changed.has('open')) {
if (this.open) {
document.addEventListener('keydown', this._onDocKeydown);
this.dispatchEvent(new CustomEvent('sema-dialog-open', { bubbles: true, composed: true }));
this.dispatchEvent(new CustomEvent('sema-open', { bubbles: true, composed: true }));
} else {
document.removeEventListener('keydown', this._onDocKeydown);
this.dispatchEvent(new CustomEvent('sema-dialog-close', { bubbles: true, composed: true }));
this.dispatchEvent(new CustomEvent('sema-close', { bubbles: true, composed: true }));
}
}
}
Expand Down
Loading