@symbo.ls/default-config
v3.14.794
Published
Default design system configuration for Symbols v3.14. Provides baseline color, typography, spacing, media query, and theme definitions used when no custom config is supplied. The atomic CSS engine (`@symbo.ls/css`) and `@symbo.ls/scratch` consume this co
Maintainers
Keywords
Readme
Default Config
Default design system configuration for Symbols v3.14. Provides baseline color, typography, spacing, media query, and theme definitions used when no custom config is supplied. The atomic CSS engine (@symbo.ls/css) and @symbo.ls/scratch consume this config to generate design tokens.
Check out the docs page to learn more.
Bridge — host a library that owns its DOM
Bridge (components/Atoms/Bridge.js) is the host for a third-party library
that owns a DOM subtree: a rich-text editor, a map, a chart, a code editor.
DOMQL owns the host node; the library owns everything inside it, and DOMQL
never writes there. Any element that declares onBridgeMount is a host;
Bridge is the named one (it keeps tag: 'div' under any key, so Map is
not an image map).
Editor: {
extends: 'Bridge',
// Once, when the node is connected (just before onMounted). Returns the instance; may be async.
onBridgeMount: async (node, el, s) => {
const { default: Quill } = await import('quill')
const box = node.ownerDocument.createElement('div')
node.appendChild(box)
return new Quill(box, { theme: 'snow' })
},
// After the mount, and again when a value it read changes (and on an empty el.update()).
onBridgeUpdate: (quill, el, s) => quill.enable(!s.locked),
// Exactly once per started mount: removal, an ancestor's removal, a route swap —
// also when an async mount resolves after the element was removed.
onBridgeDestroy: (quill) => quill.off('text-change'),
// The library's nodes: capture hears what never bubbles (a scroll inside) and runs
// before the library's own listener; selector narrows the target.
onScroll: { capture: true, passive: true, selector: '.ql-editor', handler: (e, el) => el.call('syncScroll', e) }
}el.bridgeis the instance once the mount settled (undefinedbefore and after destroy); other elements reach it throughel.lookup/el.lookdown.- An
if:-hidden host keeps its library; shown again it does not mount twice. - Browsers only: a server render (brender) leaves the host empty and runs no bridge hook; the browser mounts once the client re-creates the tree.
- Keep DOMQL children beside the host, not in it; a
texton the host stops at the mount. - The
{ capture, passive, once, selector, handler }form works for every node-levelonXxx, not only on a host. - frank-audit does not flag DOM work inside the three hooks and the host's own
onXxxhandlers (FA205, FA503–FA510, FA512); the same code anywhere else still flags.
TooltipLayer — tooltips for nodes that cannot hold a child
The built-in Tooltip is a child element that TooltipParent reveals, so a
node a library owns (a Quill toolbar button inside a Bridge host) or an
icon-only button cannot get one. TooltipLayer
(components/Tooltip/TooltipLayer.js) serves every [data-tooltip] node
inside its PARENT, whoever made it and whenever:
RichText: {
Editor: {
extends: 'Bridge',
onBridgeMount: async (node) => {
const quill = new Quill(…)
node.querySelector('.ql-bold').setAttribute('data-tooltip', 'Bold')
return quill
}
},
TooltipLayer: {} // beside the host, not inside it
},
Toolbar: {
Close: { extends: 'SquareButton', icon: 'x', aria: { label: 'Close' }, data: { tooltip: 'Close panel' } },
TooltipLayer: { tooltipPlacement: 'top' }
}- Attributes:
data-tooltip(the title; empty = none),data-tooltip-description(a second line),data-tooltip-placement(top|bottom|left|right, beats the layer'stooltipPlacement, defaultbottom). Read at every show. - Props:
tooltipPlacement,tooltipDelay(hover, ms, default 250),tooltipHideDelay(ms, default 100). Style it like any element (theme,padding, …); defaulttheme: 'dialog'. - Opens on hover after the delay (moving to the next target while one shows
switches at once) and on keyboard focus (
:focus-visible) at once; hover wins over focus; touch never opens it. Escape or a press on the target hides it until the pointer / focus moves to another target. The pointer may move onto the bubble (WCAG 1.4.13). - A
role="tooltip"node with real text and an id; the target'saria-describedbycarries that id while it shows (ids already there stay). It describes; an icon-only control still needsaria-label. Do not addtitleas well. position: fixed, centred, flipped when the side is short, kept in the viewport, followed on scroll and resize.- One layer per parent; with nested layers the innermost serves a target.
Listeners are
onDocument*/onWindowResize: removed with the element.
Dropdown — keyboard and screen reader
DropdownParent (opens on hover), DropdownParentFocus (opens while focus is
inside it) and DropdownSiblingFocus (opens the [dropdown] element after
it) reveal their Dropdown / DropdownList panel with CSS, as they always
did. On top of that they are menu buttons (WAI-ARIA APG); the documented shape
needs nothing more:
FilterMenu: {
extends: 'DropdownParent',
Button: { text: '{{ filter | polyglot }}' },
Dropdown: {
DropdownList: { options: [{ key: 'all', text: '{{ all | polyglot }}' }, { key: 'mine', text: '{{ mine | polyglot }}' }] }
}
}- The trigger is the parent itself when it renders a button (
extends: ['Button', 'DropdownParentFocus']), else its first child other than the panel andInput_trigger. A plain text element there (Label,Text) getsrole="button"andtabindex="0"; a link, a field or an icon there leaves the dropdown CSS-only (no attributes, no keys), as before. Prefer aButton. - The trigger gets
aria-expanded,aria-controls(the panel's id, assigned when it has none) andaria-haspopupwith the panel's popup role (menufor aDropdownList;listbox,tree,grid,dialoglikewise; none for a plain panel). An attribute you set yourself is kept — a factory too, even while it renders nothing. A menu without a name is labelled by the trigger. - Keys: Enter, Space and a click toggle; ArrowDown / ArrowUp open on the first
(the checked one, if any) / last item; Escape closes and returns focus to
the trigger; Tab out of a menu closes it. Focus moves in at once, while the
panel's reveal is still starting: a hidden item cannot take focus, so the
dropdown finishes the
visibilitytransitions between the item and its panel (they computevisibleonce they run anyway); the opacity / transform fade runs on. In aDropdownList(role="menu", rowsrole="menuitemradio"witharia-checked= the row'sisActive): ArrowDown / ArrowUp / Home / End, with a roving tabindex — one item in the tab order: the one focus last went to, else the first enabled item. - A click pins it open (touch has no hover); a second click, Escape or focus
leaving closes it. A choice in a
DropdownParentFocuscloses it and returns focus to the trigger (it used to drop to<body>); in aDropdownParenta choice closes it while it is open — a click or a key opened it, or focus went into the panel. A mouse press that focuses the row is focus going in, so a mouse choice closes a hover-opened menu in Chrome and Edge and leaves it open in Safari and Firefox on macOS, which do not focus a button on a press. - The parent's
data-dropdown-opencarries the explicit state: absent, the CSS decides;"true", open;"false", closed even while hovered or focused within, until focus leaves or the mouse comes back. Input_trigger, the invisible checkbox of the two focus variants, stays for the mouse; on a wired dropdown it istabindex="-1"andaria-hidden, and a child trigger also takes the wrapper's tab stop (the wrapper getstabindex="-1"). A dropdown left CSS-only keeps both as they were.DropdownListrows use thecheckicon (they asked forcheckmark, which the default icon set does not have).- The
@symbo.ls/dropdownkit'sDropdown,DropdownList,DropdownParent,DropdownParentFocusandDropdownSiblingFocusare aliases of these (extends: 'smbls.X'): one definition.
DropdownDisclosure — a menu of links and actions
A menu whose items are links and actions (a site's sections, an account menu,
a language menu) is a disclosure, not a role="menu": a menu role puts a
screen reader into application mode for a list of links.
Account: {
extends: 'DropdownDisclosure',
Button: { text: '{{ account | polyglot }}' },
Dropdown: {
Profile: { extends: 'Link', href: '/me', text: '{{ profile | polyglot }}' },
SignOut: { extends: 'Button', text: '{{ signOut | polyglot }}', onClick: (e, el) => el.call('signOut') }
}
}- The trigger (the parent itself when it is a button, else its first child
that is not the panel) gets
aria-expandedandaria-controls; noaria-haspopup. A text trigger getsrole="button"andtabindex="0". - The panel's links and buttons keep the normal tab order: no menu roles, no roving tabindex, no arrow keys.
- Click, Enter and Space toggle; Escape (on the trigger or in the panel) closes and returns focus to the trigger; focus moving out, or a press outside, closes it; a choice in the panel (a link — a router's in-place navigation too — or a button) closes it and returns focus to the trigger. A modified click (a new tab) keeps it open.
- The panel shows only while it is open (
data-dropdown-open="true"): no hover or focus-within reveal. The press-outside listener exists only while one is open.
Dialog — modal, named, translated
ConfirmDelete: {
extends: 'Dialog',
modal: (el, s) => s.root.confirmDelete, // truthy: showModal(); falsy: close()
onOk: (e, el) => el.call('deleteFile'),
onClose: (e, el, s) => s.root.update({ confirmDelete: false }),
DialogHeader: { Title: { Text: { text: '{{ deleteFile | polyglot }}' } } },
DialogFooter: {}
}modal(a value or(el, s) => …) opens the dialog withshowModal()when truthy and closes it when falsy: the top layer, an inert page behind it, Escape, focus moved in and given back by the browser. A closed modal dialog isdisplay: none.modal: truewith anif:that mounts the dialog works too (it opens once it is in the document).- Escape, Cancel, OK and the close button close the dialog themselves; keep
your state in step in
onClose.el.node.returnValueis'ok','cancel'or''(Escape). - Events on the dialog:
onCancel(Escape, Cancel, the close button) andonOk(OK);e.preventDefault()keeps it open. Any button inside can carry an action:data: { dialogAction: 'ok' }. - Named:
aria-labelledbypoints at the DialogHeader title (mark another element withdialogTitle: true); the icon-only close button is labelled. A name you give it yourself (aria-label,aria-labelledby, a factory too) is kept. - Texts are polyglot keys:
dialog.title,dialog.close,dialog.cancel,dialog.ok. A key your project has not translated shows the framework default for the active language: English (Title, Close, Cancel, OK) or Georgian (სათაური, დახურვა, გაუქმება, კარგი); any other language, and a project without polyglot, shows English. The defaults (DIALOG_COPY) and their resolver (resolveDialogCopy) are exported from this package and fromsmbls; the windows plugin's Confirm / Alert / Prompt read the same table, so one translation ofdialog.cancelserves every framework dialog. - Without
modalnothing changes on screen: the dialog renders in the flow (display: flex, noopen), and a flatopen: trueis still the native non-modal attribute. locked(a value or(el, s) => …) makes a dialog the person cannot dismiss — a forced password change, an invitation: Escape does nothing (thecancelevent and the Escape key itself are prevented — a browser lets a page preventcancelonly once without a new user activation —, andclosedby="none"is written), a press on the backdrop does nothing, and the built-in cancel controls (the header's close button, Cancel) do not render. OK still firesonOk. It closes from code:modalgoing false, orel.node.close(). Only a dialog that declareslockedgets the listeners.
