openapi-nm-design-system.yaml
Rama dev · réplica de lectura del repositorio original.
openapi: 3.0.3
info:
title: NoMercy NM Design System Components
description: |
The server-driven component vocabulary, generated from the design system.
This file is a sibling of `openapi-nm-components.yaml` and follows the same
rules: an envelope carrying `id`, `component` and `props`, a discriminator on
`component`, nested children under `items`, and a free-form `properties` bag
for anything a payload needs to carry that the contract has not named yet.
What this file adds is the box. Every component here accepts an `NMBox` and
an `NMSurface`, which is what makes them compose: a parent can place a child
without the child knowing anything about where it was placed, and a plugin
can lay out a dashboard entirely from the payload.
The media-domain components in `openapi-nm-components.yaml` are unchanged.
They keep their existing shape and their existing clients keep working. The
versions of those components that adopt this box live here, under the same
names, and a client opts in by rendering from this vocabulary.
Nothing here carries a raw CSS value. Every length, colour and radius is a
token key resolved by the client, which is the only reason the same payload
can render on a platform that has no CSS.
version: 1.0.0
components:
schemas:
# ===========================================
# The box
# ===========================================
NMBox:
type: object
description: |
Geometry, split by who owns it.
The outer group is what a parent decides about this component: its
margin, how it flexes, how it aligns itself, how many columns it spans,
how big it is. A parent sets these on a child it is placing.
The inner group is what this component decides about its own children:
padding, gap, direction, alignment. A component sets these on itself.
Both groups are optional and both are overridable by the payload. The
rule that makes this reliable is not in the shape of this object, it is
in the order the client applies it: a value from the payload is applied
after the component's own default, so the payload always wins. A client
that resolves them in the other order will look correct until the first
time a parent tries to override a child, which is the failure that would
otherwise ship silently.
properties:
# ---- outer: what the parent decides ----
margin:
$ref: '#/components/schemas/NMSpacing'
width:
$ref: '#/components/schemas/NMSize'
height:
$ref: '#/components/schemas/NMSize'
min_width:
$ref: '#/components/schemas/NMSize'
max_width:
$ref: '#/components/schemas/NMSize'
min_height:
$ref: '#/components/schemas/NMSize'
max_height:
$ref: '#/components/schemas/NMSize'
grow:
type: integer
nullable: true
minimum: 0
description: How much of the leftover space along the parent's axis this takes.
shrink:
type: integer
nullable: true
minimum: 0
description: How readily this gives up space when the parent runs short.
basis:
$ref: '#/components/schemas/NMSize'
align_self:
$ref: '#/components/schemas/NMAlign'
order:
type: integer
nullable: true
description: Visual position among siblings, independent of payload order.
column_span:
type: integer
nullable: true
minimum: 1
description: Columns this occupies when the parent is a grid.
row_span:
type: integer
nullable: true
minimum: 1
description: Rows this occupies when the parent is a grid.
position:
$ref: '#/components/schemas/NMPosition'
inset:
$ref: '#/components/schemas/NMSpacing'
layer:
$ref: '#/components/schemas/NMLayer'
hidden_on:
type: array
items:
$ref: '#/components/schemas/NMSurfaceKind'
default: []
description: |
Surfaces this component is not rendered on at all. A component hidden
on tv is never focusable there, which is different from a component
that is merely invisible, and is what D-pad traversal needs.
# ---- inner: what this component decides ----
padding:
$ref: '#/components/schemas/NMSpacing'
gap:
$ref: '#/components/schemas/NMGap'
direction:
$ref: '#/components/schemas/NMDirection'
align:
$ref: '#/components/schemas/NMAlign'
justify:
$ref: '#/components/schemas/NMJustify'
wrap:
$ref: '#/components/schemas/NMWrap'
columns:
type: integer
nullable: true
minimum: 1
description: Fixed column count when this component lays its children out as a grid.
overflow:
$ref: '#/components/schemas/NMOverflow'
NMSpacing:
type: object
description: |
A spacing value, expressed per side. Resolution runs from the most
general to the most specific: `all`, then `x` and `y`, then a named side.
A later value replaces an earlier one for the side it names.
Every value is a key on the spacing scale, not a length. `"4"` means the
scale's step 4, which the client resolves to its own unit. Writing a
length here is not supported, because a length cannot survive the trip to
a platform whose unit is not the one it was written in.
properties:
all:
$ref: '#/components/schemas/NMSpace'
x:
$ref: '#/components/schemas/NMSpace'
y:
$ref: '#/components/schemas/NMSpace'
top:
$ref: '#/components/schemas/NMSpace'
right:
$ref: '#/components/schemas/NMSpace'
bottom:
$ref: '#/components/schemas/NMSpace'
left:
$ref: '#/components/schemas/NMSpace'
NMGap:
type: object
description: |
The space between children, which can differ along each axis. `all` sets
both; `x` and `y` override it per axis.
properties:
all:
$ref: '#/components/schemas/NMSpace'
x:
$ref: '#/components/schemas/NMSpace'
y:
$ref: '#/components/schemas/NMSpace'
NMSpace:
type: string
nullable: true
description: |
A key on the spacing scale. The scale is the design system's, so a key
that is not on it is a payload error rather than something the client
should try to interpret.
enum:
- '0'
- px
- '0-5'
- '1'
- '1-5'
- '2'
- '2-5'
- '3'
- '3-5'
- '4'
- '4-5'
- '5'
- '6'
- '7'
- '8'
- '9'
- '10'
- '11'
- '12'
- '14'
- '15'
- '16'
- '20'
- '24'
- '28'
- '32'
- '36'
- '40'
- '44'
- '48'
- '52'
- '56'
- '60'
- '64'
- '72'
- '80'
- '96'
NMSize:
type: string
nullable: true
description: |
A size. Either a key on the spacing scale, a fraction of the parent, or a
keyword. Fractions are written as they read: `1/2`, `2/3`, `5/6`.
`content` sizes to what is inside. `available` fills what the parent
offers. `auto` lets the platform decide, which is the default when the
field is absent.
pattern: '^(0|px|\d+(-\d)?|\d+/\d+|auto|full|available|content|min|max|screen)$'
NMAlign:
type: string
nullable: true
description: Alignment across the axis that runs against the layout direction.
enum: [start, center, end, stretch, baseline]
NMJustify:
type: string
nullable: true
description: Distribution along the layout direction.
enum: [start, center, end, between, around, evenly]
NMDirection:
type: string
nullable: true
description: |
The axis children flow along. `row` and `column` are resolved against the
reading direction, so a right-to-left locale reverses `row` without the
payload saying anything about it.
enum: [row, column, row-reverse, column-reverse]
NMWrap:
type: string
nullable: true
enum: [nowrap, wrap, wrap-reverse]
NMOverflow:
type: string
nullable: true
description: |
What happens to children that do not fit. `scroll` on a tv surface means
the container scrolls to follow focus, since there is no pointer to drag
it with.
enum: [visible, hidden, scroll, clip]
NMPosition:
type: string
nullable: true
enum: [static, relative, absolute, sticky, fixed]
NMLayer:
type: string
nullable: true
description: |
Stacking order, named rather than numbered. A payload that could set a
raw z-index would eventually set one that beats the player chrome, so the
vocabulary is closed on purpose.
enum: [base, raised, sticky, overlay, modal, toast, tooltip]
NMSurfaceKind:
type: string
description: A rendering surface, used to vary or suppress a component per platform.
enum: [web, mobile, tv]
# ===========================================
# The skin
# ===========================================
NMSurface:
type: object
description: |
Appearance, kept apart from geometry so a component can be re-skinned
without touching its layout and re-laid-out without touching its skin.
Colours are token keys, never hex. A payload carrying hex would render
correctly in one theme and wrongly in the other, and would not respond
when the user switches.
properties:
background:
$ref: '#/components/schemas/NMColor'
foreground:
$ref: '#/components/schemas/NMColor'
border_color:
$ref: '#/components/schemas/NMColor'
border_width:
type: string
nullable: true
enum: ['0', px, '2', '4', '8']
radius:
type: string
nullable: true
enum: [none, sm, base, md, lg, xl, 2xl, 3xl, full]
shadow:
type: string
nullable: true
enum: [none, xs, sm, md, lg, xl, 2xl, inner]
opacity:
type: integer
nullable: true
minimum: 0
maximum: 100
elevation:
type: string
nullable: true
description: |
How the component lifts when it takes focus. This is the tv surface's
equivalent of hover, which does not exist there.
enum: [none, subtle, raised, floating]
NMColor:
type: string
nullable: true
description: |
A colour token key from the design system, without the leading dashes.
Both a semantic name and a scale step are accepted, so `surface-1` and
`teal-9` are both valid, and both resolve per theme.
# ===========================================
# Behaviour shared by every component
# ===========================================
NMAction:
type: object
description: |
Something the client should do when the component is activated. An action
names an intent; it never carries code. A client that does not understand
an action ignores it rather than guessing, which is what lets a newer
server talk to an older client without breaking it.
required:
- kind
properties:
kind:
type: string
description: The intent, such as navigate, play, mutate, dismiss or submit.
target:
type: string
nullable: true
description: What the intent applies to, usually a link or a component id.
body:
type: object
additionalProperties: true
nullable: true
confirm:
type: string
nullable: true
description: When set, the client confirms with this message before acting.
NMAccessibility:
type: object
description: |
The accessible name and role a payload can set. Every component ships a
correct default, so this exists to override, not to supply what would
otherwise be missing. A component that needs this field filled in to be
accessible is a component with a bug.
properties:
label:
type: string
nullable: true
described_by:
type: string
nullable: true
role:
type: string
nullable: true
live:
type: string
nullable: true
enum: [off, polite, assertive]
# ===========================================
# Content a component draws rather than a way it looks
# ===========================================
NMTableCellKind:
type: string
description: |
How a cell draws what its column points at. The design system builds the
cell itself from this, which is why a table is described rather than
assembled: a server that had to nest an avatar, a badge and a checkbox
per row would restate the table's own anatomy on every screen.
enum: [text, avatar-text, badge, rating, checkbox, actions]
NMTableColumn:
type: object
description: |
One column: what it is called, which field of a row it reads, and how
that field draws.
required:
- label
- key
- type
properties:
label:
type: string
description: The header text. Empty for a column whose header is a control.
key:
type: string
description: The row field this column reads.
type:
$ref: '#/components/schemas/NMTableCellKind'
sortable:
type: boolean
nullable: true
description: When true the header is a sort button carrying aria-sort.
NMTableRow:
type: object
description: |
One row, as the fields its columns read. Values are flat on purpose: a
cell's appearance is the column's decision, so a row carries data and
never markup.
additionalProperties: true
NMSelectOption:
type: object
description: |
One choice in a select or a combobox: what a reader sees and what the
form submits.
required:
- label
- value
properties:
label:
type: string
value:
type: string
NMChatMessage:
type: object
description: |
One message in a conversation. `variant` is who is speaking, which is
what decides the side it sits on and the colour it takes.
required:
- variant
- text
properties:
variant:
type: string
enum: [sent, received]
text:
type: string
time:
type: string
nullable: true
avatar:
type: string
nullable: true
NMStep:
type: object
description: One step of a stepper, and what it says about itself.
required:
- label
properties:
label:
type: string
description:
type: string
nullable: true
# ===========================================
# The base every component's props extend
# ===========================================
NMComponentBase:
type: object
description: |
What every component in this vocabulary carries, whatever it draws.
`items` is here rather than on the container components alone, because a
brick that cannot hold children is not a brick. A component that has no
meaningful place to put children renders them after its own content.
required:
- id
properties:
id:
type: string
items:
type: array
items:
$ref: '#/components/schemas/DesignComponent'
default: []
description: Nested children, rendered inside this component.
slots:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/DesignComponent'
default: {}
description: |
Children for a slot the component names, keyed by that name. A card
declares a header, a body and a footer; without this a payload could
reach none of them, because every renderer filled the default slot
and nothing else. Anything sent under a name the component does not
declare is ignored rather than drawn somewhere it does not belong.
box:
$ref: '#/components/schemas/NMBox'
surface:
$ref: '#/components/schemas/NMSurface'
color:
type: string
nullable: true
description: |
The palette family this component draws its accent from, such as
`violet` or `red`. Any family the design system ships is accepted,
not only the handful a given component enumerates: those are the
designer's recommendation rather than the limit.
Absent, the component follows the accent the app is set to, which is
the behaviour a payload should want almost always. Naming one here
is for the case where a single component has to stand apart from the
rest of the interface.
action:
$ref: '#/components/schemas/NMAction'
accessibility:
$ref: '#/components/schemas/NMAccessibility'
test_id:
type: string
nullable: true
description: |
A stable handle for tests. The box suites read the rendered geometry
through this, which is why it is part of the contract rather than
something each client invents.
next_id:
type: string
nullable: true
description: The component focus moves to next, for D-pad traversal.
previous_id:
type: string
nullable: true
context_menu_items:
type: array
items:
$ref: '#/components/schemas/DesignContextMenuItem'
default: []
url:
type: string
nullable: true
description: Where this component's data is fetched or refreshed from.
properties:
type: object
additionalProperties: true
nullable: true
description: |
Anything the contract has not named yet. This is the escape hatch that
keeps an older client alive against a newer server, and it is not a
place to put something that deserves a name.
DesignContextMenuItem:
type: object
properties:
id:
type: string
nullable: true
title:
type: string
nullable: true
icon:
type: string
nullable: true
destructive:
type: boolean
default: false
action:
$ref: '#/components/schemas/NMAction'
DesignComponent:
type: object
description: |
The envelope, matching `Component` in `openapi-nm-components.yaml`. The
`component` field discriminates, and `props` is that component's own
schema, every one of which extends `NMComponentBase`.
The concrete component schemas are generated from the design system into
`openapi-nm-design-system.components.yaml` and are not hand-written here,
because a contract maintained by hand alongside a generated one drifts.
required:
- id
- component
- props
properties:
id:
type: string
component:
type: string
description: Component type discriminator, such as NMButton or NMCard.
props:
type: object
additionalProperties: true
description: The component's props, extending NMComponentBase.
update:
$ref: '#/components/schemas/DesignUpdate'
DesignUpdate:
type: object
description: When and from where this component refreshes itself.
properties:
when:
type: string
nullable: true
link:
type: string
nullable: true
body:
type: object
additionalProperties: true
nullable: true