@tai42/studio-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +318 -0
- package/SECURITY.md +51 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.js +14 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/checkbox.d.ts +24 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +31 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/code-block.d.ts +7 -0
- package/dist/components/code-block.d.ts.map +1 -0
- package/dist/components/code-block.js +23 -0
- package/dist/components/code-block.js.map +1 -0
- package/dist/components/completion-input.d.ts +31 -0
- package/dist/components/completion-input.d.ts.map +1 -0
- package/dist/components/completion-input.js +61 -0
- package/dist/components/completion-input.js.map +1 -0
- package/dist/components/components.css +2212 -0
- package/dist/components/confirm-dialog.d.ts +40 -0
- package/dist/components/confirm-dialog.d.ts.map +1 -0
- package/dist/components/confirm-dialog.js +11 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/control-styles.d.ts +12 -0
- package/dist/components/control-styles.d.ts.map +1 -0
- package/dist/components/control-styles.js +14 -0
- package/dist/components/control-styles.js.map +1 -0
- package/dist/components/copy-field.d.ts +8 -0
- package/dist/components/copy-field.d.ts.map +1 -0
- package/dist/components/copy-field.js +106 -0
- package/dist/components/copy-field.js.map +1 -0
- package/dist/components/dialog.d.ts +21 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +25 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/drawer.d.ts +22 -0
- package/dist/components/drawer.d.ts.map +1 -0
- package/dist/components/drawer.js +32 -0
- package/dist/components/drawer.js.map +1 -0
- package/dist/components/error-boundary.d.ts +33 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/error-boundary.js +36 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/extension-combo-builder.d.ts +70 -0
- package/dist/components/extension-combo-builder.d.ts.map +1 -0
- package/dist/components/extension-combo-builder.js +154 -0
- package/dist/components/extension-combo-builder.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +65 -0
- package/dist/components/extension-grouping.d.ts.map +1 -0
- package/dist/components/extension-grouping.js +78 -0
- package/dist/components/extension-grouping.js.map +1 -0
- package/dist/components/extension-picker.d.ts +35 -0
- package/dist/components/extension-picker.d.ts.map +1 -0
- package/dist/components/extension-picker.js +31 -0
- package/dist/components/extension-picker.js.map +1 -0
- package/dist/components/external-link.d.ts +19 -0
- package/dist/components/external-link.d.ts.map +1 -0
- package/dist/components/external-link.js +12 -0
- package/dist/components/external-link.js.map +1 -0
- package/dist/components/field.d.ts +64 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.js +56 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/fleet-report.d.ts +39 -0
- package/dist/components/fleet-report.d.ts.map +1 -0
- package/dist/components/fleet-report.js +50 -0
- package/dist/components/fleet-report.js.map +1 -0
- package/dist/components/fonts.css +22 -0
- package/dist/components/icons.d.ts +142 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +139 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/inputs.d.ts +52 -0
- package/dist/components/inputs.d.ts.map +1 -0
- package/dist/components/inputs.js +35 -0
- package/dist/components/inputs.js.map +1 -0
- package/dist/components/json-diff.d.ts +40 -0
- package/dist/components/json-diff.d.ts.map +1 -0
- package/dist/components/json-diff.js +119 -0
- package/dist/components/json-diff.js.map +1 -0
- package/dist/components/json-tree.d.ts +8 -0
- package/dist/components/json-tree.d.ts.map +1 -0
- package/dist/components/json-tree.js +70 -0
- package/dist/components/json-tree.js.map +1 -0
- package/dist/components/modal-focus.d.ts +15 -0
- package/dist/components/modal-focus.d.ts.map +1 -0
- package/dist/components/modal-focus.js +136 -0
- package/dist/components/modal-focus.js.map +1 -0
- package/dist/components/page-header.d.ts +56 -0
- package/dist/components/page-header.d.ts.map +1 -0
- package/dist/components/page-header.js +24 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/primitives.d.ts +186 -0
- package/dist/components/primitives.d.ts.map +1 -0
- package/dist/components/primitives.js +204 -0
- package/dist/components/primitives.js.map +1 -0
- package/dist/components/progress-bar.d.ts +29 -0
- package/dist/components/progress-bar.d.ts.map +1 -0
- package/dist/components/progress-bar.js +48 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/radio-group.d.ts +37 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +56 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/reveal-input.d.ts +13 -0
- package/dist/components/reveal-input.d.ts.map +1 -0
- package/dist/components/reveal-input.js +31 -0
- package/dist/components/reveal-input.js.map +1 -0
- package/dist/components/scroll-region.d.ts +85 -0
- package/dist/components/scroll-region.d.ts.map +1 -0
- package/dist/components/scroll-region.js +574 -0
- package/dist/components/scroll-region.js.map +1 -0
- package/dist/components/select.d.ts +53 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +33 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/table.d.ts +59 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +20 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/tabs.d.ts +15 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +13 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/tags.d.ts +46 -0
- package/dist/components/tags.d.ts.map +1 -0
- package/dist/components/tags.js +68 -0
- package/dist/components/tags.js.map +1 -0
- package/dist/components/tokens.css +392 -0
- package/dist/components/tokens.d.ts +15 -0
- package/dist/components/tokens.d.ts.map +1 -0
- package/dist/components/tokens.js +101 -0
- package/dist/components/tokens.js.map +1 -0
- package/dist/components/tool-picker.d.ts +24 -0
- package/dist/components/tool-picker.d.ts.map +1 -0
- package/dist/components/tool-picker.js +97 -0
- package/dist/components/tool-picker.js.map +1 -0
- package/dist/components/tooltip.d.ts +17 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +18 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/version-history-panel.d.ts +71 -0
- package/dist/components/version-history-panel.d.ts.map +1 -0
- package/dist/components/version-history-panel.js +148 -0
- package/dist/components/version-history-panel.js.map +1 -0
- package/dist/download.d.ts +7 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +18 -0
- package/dist/download.js.map +1 -0
- package/dist/element-slot.d.ts +4 -0
- package/dist/element-slot.d.ts.map +1 -0
- package/dist/element-slot.js +20 -0
- package/dist/element-slot.js.map +1 -0
- package/dist/elicitation/ElicitationForm.d.ts +34 -0
- package/dist/elicitation/ElicitationForm.d.ts.map +1 -0
- package/dist/elicitation/ElicitationForm.js +24 -0
- package/dist/elicitation/ElicitationForm.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension-combos.d.ts +13 -0
- package/dist/extension-combos.d.ts.map +1 -0
- package/dist/extension-combos.js +9 -0
- package/dist/extension-combos.js.map +1 -0
- package/dist/hooks/useApi.d.ts +1222 -0
- package/dist/hooks/useApi.d.ts.map +1 -0
- package/dist/hooks/useApi.js +22 -0
- package/dist/hooks/useApi.js.map +1 -0
- package/dist/hooks/useAuth.d.ts +13 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useAuth.js +53 -0
- package/dist/hooks/useAuth.js.map +1 -0
- package/dist/hooks/useBreakpoint.d.ts +10 -0
- package/dist/hooks/useBreakpoint.d.ts.map +1 -0
- package/dist/hooks/useBreakpoint.js +66 -0
- package/dist/hooks/useBreakpoint.js.map +1 -0
- package/dist/hooks/useCapabilities.d.ts +88 -0
- package/dist/hooks/useCapabilities.d.ts.map +1 -0
- package/dist/hooks/useCapabilities.js +203 -0
- package/dist/hooks/useCapabilities.js.map +1 -0
- package/dist/hooks/useSse.d.ts +13 -0
- package/dist/hooks/useSse.d.ts.map +1 -0
- package/dist/hooks/useSse.js +298 -0
- package/dist/hooks/useSse.js.map +1 -0
- package/dist/hooks/useTheme.d.ts +20 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/hooks/useTheme.js +97 -0
- package/dist/hooks/useTheme.js.map +1 -0
- package/dist/hooks/useUnauthorized.d.ts +3 -0
- package/dist/hooks/useUnauthorized.d.ts.map +1 -0
- package/dist/hooks/useUnauthorized.js +18 -0
- package/dist/hooks/useUnauthorized.js.map +1 -0
- package/dist/host.d.ts +11 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +10 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/context.d.ts +25 -0
- package/dist/navigation/context.d.ts.map +1 -0
- package/dist/navigation/context.js +45 -0
- package/dist/navigation/context.js.map +1 -0
- package/dist/navigation/index.d.ts +4 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/types.d.ts +97 -0
- package/dist/navigation/types.d.ts.map +1 -0
- package/dist/navigation/types.js +16 -0
- package/dist/navigation/types.js.map +1 -0
- package/dist/plugin/host-state.d.ts +45 -0
- package/dist/plugin/host-state.d.ts.map +1 -0
- package/dist/plugin/host-state.js +62 -0
- package/dist/plugin/host-state.js.map +1 -0
- package/dist/plugin/registry.d.ts +35 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/registry.js +129 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/types.d.ts +165 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin/types.js +2 -0
- package/dist/plugin/types.js.map +1 -0
- package/dist/plugin/version.d.ts +34 -0
- package/dist/plugin/version.d.ts.map +1 -0
- package/dist/plugin/version.js +37 -0
- package/dist/plugin/version.js.map +1 -0
- package/dist/query-keys.d.ts +31 -0
- package/dist/query-keys.d.ts.map +1 -0
- package/dist/query-keys.js +31 -0
- package/dist/query-keys.js.map +1 -0
- package/dist/schema-editor/SchemaEditor.d.ts +43 -0
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -0
- package/dist/schema-editor/SchemaEditor.js +106 -0
- package/dist/schema-editor/SchemaEditor.js.map +1 -0
- package/dist/schema-editor/index.d.ts +10 -0
- package/dist/schema-editor/index.d.ts.map +1 -0
- package/dist/schema-editor/index.js +8 -0
- package/dist/schema-editor/index.js.map +1 -0
- package/dist/schema-editor/lint.d.ts +37 -0
- package/dist/schema-editor/lint.d.ts.map +1 -0
- package/dist/schema-editor/lint.js +49 -0
- package/dist/schema-editor/lint.js.map +1 -0
- package/dist/schema-form/SchemaForm.d.ts +70 -0
- package/dist/schema-form/SchemaForm.d.ts.map +1 -0
- package/dist/schema-form/SchemaForm.js +13 -0
- package/dist/schema-form/SchemaForm.js.map +1 -0
- package/dist/schema-form/array-field.d.ts +20 -0
- package/dist/schema-form/array-field.d.ts.map +1 -0
- package/dist/schema-form/array-field.js +39 -0
- package/dist/schema-form/array-field.js.map +1 -0
- package/dist/schema-form/classify.d.ts +87 -0
- package/dist/schema-form/classify.d.ts.map +1 -0
- package/dist/schema-form/classify.js +199 -0
- package/dist/schema-form/classify.js.map +1 -0
- package/dist/schema-form/context.d.ts +4 -0
- package/dist/schema-form/context.d.ts.map +1 -0
- package/dist/schema-form/context.js +14 -0
- package/dist/schema-form/context.js.map +1 -0
- package/dist/schema-form/default-value.d.ts +8 -0
- package/dist/schema-form/default-value.d.ts.map +1 -0
- package/dist/schema-form/default-value.js +64 -0
- package/dist/schema-form/default-value.js.map +1 -0
- package/dist/schema-form/enum-field.d.ts +19 -0
- package/dist/schema-form/enum-field.d.ts.map +1 -0
- package/dist/schema-form/enum-field.js +26 -0
- package/dist/schema-form/enum-field.js.map +1 -0
- package/dist/schema-form/field-group.d.ts +9 -0
- package/dist/schema-form/field-group.d.ts.map +1 -0
- package/dist/schema-form/field-group.js +32 -0
- package/dist/schema-form/field-group.js.map +1 -0
- package/dist/schema-form/field-node.d.ts +24 -0
- package/dist/schema-form/field-node.d.ts.map +1 -0
- package/dist/schema-form/field-node.js +51 -0
- package/dist/schema-form/field-node.js.map +1 -0
- package/dist/schema-form/index.d.ts +13 -0
- package/dist/schema-form/index.d.ts.map +1 -0
- package/dist/schema-form/index.js +11 -0
- package/dist/schema-form/index.js.map +1 -0
- package/dist/schema-form/media-field.d.ts +24 -0
- package/dist/schema-form/media-field.d.ts.map +1 -0
- package/dist/schema-form/media-field.js +169 -0
- package/dist/schema-form/media-field.js.map +1 -0
- package/dist/schema-form/media.d.ts +40 -0
- package/dist/schema-form/media.d.ts.map +1 -0
- package/dist/schema-form/media.js +65 -0
- package/dist/schema-form/media.js.map +1 -0
- package/dist/schema-form/object-fields.d.ts +20 -0
- package/dist/schema-form/object-fields.d.ts.map +1 -0
- package/dist/schema-form/object-fields.js +24 -0
- package/dist/schema-form/object-fields.js.map +1 -0
- package/dist/schema-form/resolve.d.ts +41 -0
- package/dist/schema-form/resolve.d.ts.map +1 -0
- package/dist/schema-form/resolve.js +102 -0
- package/dist/schema-form/resolve.js.map +1 -0
- package/dist/schema-form/string-field.d.ts +20 -0
- package/dist/schema-form/string-field.d.ts.map +1 -0
- package/dist/schema-form/string-field.js +68 -0
- package/dist/schema-form/string-field.js.map +1 -0
- package/dist/schema-form/styles.d.ts +18 -0
- package/dist/schema-form/styles.d.ts.map +1 -0
- package/dist/schema-form/styles.js +18 -0
- package/dist/schema-form/styles.js.map +1 -0
- package/dist/schema-form/types.d.ts +67 -0
- package/dist/schema-form/types.d.ts.map +1 -0
- package/dist/schema-form/types.js +10 -0
- package/dist/schema-form/types.js.map +1 -0
- package/dist/schema-form/union-field.d.ts +24 -0
- package/dist/schema-form/union-field.d.ts.map +1 -0
- package/dist/schema-form/union-field.js +55 -0
- package/dist/schema-form/union-field.js.map +1 -0
- package/dist/schema-form/union.d.ts +4 -0
- package/dist/schema-form/union.d.ts.map +1 -0
- package/dist/schema-form/union.js +68 -0
- package/dist/schema-form/union.js.map +1 -0
- package/dist/schema-form/unsupported-notice.d.ts +18 -0
- package/dist/schema-form/unsupported-notice.d.ts.map +1 -0
- package/dist/schema-form/unsupported-notice.js +8 -0
- package/dist/schema-form/unsupported-notice.js.map +1 -0
- package/dist/schema-form/validate.d.ts +18 -0
- package/dist/schema-form/validate.d.ts.map +1 -0
- package/dist/schema-form/validate.js +162 -0
- package/dist/schema-form/validate.js.map +1 -0
- package/dist/structured-output/StructuredOutput.d.ts +27 -0
- package/dist/structured-output/StructuredOutput.d.ts.map +1 -0
- package/dist/structured-output/StructuredOutput.js +41 -0
- package/dist/structured-output/StructuredOutput.js.map +1 -0
- package/dist/testing.d.ts +80 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +207 -0
- package/dist/testing.js.map +1 -0
- package/package.json +101 -0
- package/src/components/badge.tsx +31 -0
- package/src/components/checkbox.tsx +93 -0
- package/src/components/code-block.tsx +37 -0
- package/src/components/completion-input.tsx +137 -0
- package/src/components/components.css +2212 -0
- package/src/components/confirm-dialog.tsx +77 -0
- package/src/components/control-styles.ts +14 -0
- package/src/components/copy-field.tsx +178 -0
- package/src/components/dialog.tsx +76 -0
- package/src/components/drawer.tsx +83 -0
- package/src/components/error-boundary.tsx +56 -0
- package/src/components/extension-combo-builder.tsx +305 -0
- package/src/components/extension-grouping.ts +116 -0
- package/src/components/extension-picker.tsx +97 -0
- package/src/components/external-link.tsx +40 -0
- package/src/components/field.tsx +169 -0
- package/src/components/fleet-report.tsx +136 -0
- package/src/components/fonts.css +22 -0
- package/src/components/icons.tsx +504 -0
- package/src/components/inputs.tsx +125 -0
- package/src/components/json-diff.tsx +192 -0
- package/src/components/json-tree.tsx +129 -0
- package/src/components/modal-focus.ts +150 -0
- package/src/components/page-header.tsx +107 -0
- package/src/components/primitives.tsx +489 -0
- package/src/components/progress-bar.tsx +100 -0
- package/src/components/radio-group.tsx +160 -0
- package/src/components/reveal-input.tsx +87 -0
- package/src/components/scroll-region.tsx +703 -0
- package/src/components/select.tsx +149 -0
- package/src/components/table.tsx +94 -0
- package/src/components/tabs.tsx +51 -0
- package/src/components/tags.tsx +137 -0
- package/src/components/tokens.css +392 -0
- package/src/components/tokens.ts +102 -0
- package/src/components/tool-picker.tsx +175 -0
- package/src/components/tooltip.tsx +46 -0
- package/src/components/version-history-panel.tsx +407 -0
- package/src/css.d.ts +2 -0
- package/src/download.ts +17 -0
- package/src/element-slot.ts +22 -0
- package/src/elicitation/ElicitationForm.tsx +87 -0
- package/src/errors.ts +12 -0
- package/src/extension-combos.ts +18 -0
- package/src/hooks/useApi.ts +27 -0
- package/src/hooks/useAuth.ts +67 -0
- package/src/hooks/useBreakpoint.ts +78 -0
- package/src/hooks/useCapabilities.ts +242 -0
- package/src/hooks/useSse.ts +314 -0
- package/src/hooks/useTheme.ts +133 -0
- package/src/hooks/useUnauthorized.ts +20 -0
- package/src/host.ts +15 -0
- package/src/index.ts +267 -0
- package/src/navigation/context.tsx +90 -0
- package/src/navigation/index.ts +9 -0
- package/src/navigation/types.ts +90 -0
- package/src/plugin/host-state.ts +93 -0
- package/src/plugin/registry.ts +174 -0
- package/src/plugin/types.ts +178 -0
- package/src/plugin/version.ts +44 -0
- package/src/query-keys.ts +34 -0
- package/src/schema-editor/SchemaEditor.tsx +186 -0
- package/src/schema-editor/index.ts +9 -0
- package/src/schema-editor/lint.ts +70 -0
- package/src/schema-form/SchemaForm.tsx +107 -0
- package/src/schema-form/array-field.tsx +110 -0
- package/src/schema-form/classify.ts +302 -0
- package/src/schema-form/context.ts +17 -0
- package/src/schema-form/default-value.ts +65 -0
- package/src/schema-form/enum-field.tsx +65 -0
- package/src/schema-form/field-group.tsx +71 -0
- package/src/schema-form/field-node.tsx +161 -0
- package/src/schema-form/index.ts +12 -0
- package/src/schema-form/media-field.tsx +281 -0
- package/src/schema-form/media.ts +69 -0
- package/src/schema-form/object-fields.tsx +74 -0
- package/src/schema-form/resolve.ts +114 -0
- package/src/schema-form/string-field.tsx +161 -0
- package/src/schema-form/styles.ts +20 -0
- package/src/schema-form/types.ts +71 -0
- package/src/schema-form/union-field.tsx +122 -0
- package/src/schema-form/union.ts +78 -0
- package/src/schema-form/unsupported-notice.tsx +39 -0
- package/src/schema-form/validate.ts +183 -0
- package/src/structured-output/StructuredOutput.tsx +81 -0
- package/src/testing.ts +235 -0
|
@@ -0,0 +1,2212 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The design-system COMPONENT + LAYOUT layer: the real classes every Studio
|
|
3
|
+
* surface is built from. It is the only place in the monorepo that may ship
|
|
4
|
+
* COMPONENT CSS — `tokens.css` and `fonts.css` are the design system's other two
|
|
5
|
+
* published sheets, and `apps/studio/src/styles.css` is the shell's own entry;
|
|
6
|
+
* feature packages compile with `tsc` and `sideEffects: false`, so a stylesheet
|
|
7
|
+
* inside one would never reach a bundle. Screens therefore apply these classes BY
|
|
8
|
+
* NAME rather than re-deriving geometry inline.
|
|
9
|
+
*
|
|
10
|
+
* Everything resolves from `tokens.css`; there are no literal colors, radii, or
|
|
11
|
+
* shadows below.
|
|
12
|
+
*
|
|
13
|
+
* THE FIVE-STATE CONTRACT, enforced off this sheet by `sheet-contract.test.ts`.
|
|
14
|
+
* A class that presents itself as a control — it declares `cursor: pointer` at
|
|
15
|
+
* rest — carries every one of:
|
|
16
|
+
* rest its own rule;
|
|
17
|
+
* hover a rule that fires while THAT class is hovered — the response
|
|
18
|
+
* may land on a child, which is what a label does for its box;
|
|
19
|
+
* active a press rendering, required of a control whose hover MOVES it
|
|
20
|
+
* and of no other: `.tai-btn` lifts, so it has to come back
|
|
21
|
+
* down, and nothing else in this sheet moves under the pointer;
|
|
22
|
+
* unavailable a `:disabled` / `[aria-disabled]` / `[data-disabled]` rule;
|
|
23
|
+
* focus-visible membership of the one shared ring rule below.
|
|
24
|
+
* The gate carries the exemptions by name with a reason each, so the next
|
|
25
|
+
* omission is a failure rather than a silent gap. Focus is a 2 px outline at a
|
|
26
|
+
* 2 px offset, never a box-shadow, because an outline follows `border-radius`
|
|
27
|
+
* and takes no part in layout.
|
|
28
|
+
*
|
|
29
|
+
* Breakpoints, desktop-first with real mobile: <640 phone, 640-1023 compact,
|
|
30
|
+
* 1024-1279 medium, >=1280 full.
|
|
31
|
+
*/
|
|
32
|
+
@layer tai-components {
|
|
33
|
+
/* -- Global typography ---------------------------------------------------
|
|
34
|
+
Headings are black and tight in every context; the host preflight resets
|
|
35
|
+
their size and weight, and this layer sorts after it. Sizes follow the
|
|
36
|
+
scale so an unclassed heading is still on-system; a component class or an
|
|
37
|
+
explicit style overrides them.
|
|
38
|
+
|
|
39
|
+
No `font-feature-settings`. None of the thirteen `.woff2` faces this design
|
|
40
|
+
system ships carries a character-variant or stylistic-set feature at all —
|
|
41
|
+
the Inter latin subset's whole feature list is `calt ccmp dnom frac locl
|
|
42
|
+
numr pnum tnum` — so naming one selects nothing and only reads as a spec
|
|
43
|
+
that is being met. The gate holds the sheet to that. */
|
|
44
|
+
h1,
|
|
45
|
+
h2,
|
|
46
|
+
h3,
|
|
47
|
+
h4,
|
|
48
|
+
h5,
|
|
49
|
+
h6 {
|
|
50
|
+
color: var(--tai-color-heading);
|
|
51
|
+
font-family: var(--tai-font-sans);
|
|
52
|
+
font-weight: 700;
|
|
53
|
+
letter-spacing: -0.03em;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
h1 {
|
|
57
|
+
font-size: var(--tai-text-xl);
|
|
58
|
+
line-height: 1.2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
h2 {
|
|
62
|
+
font-size: var(--tai-text-section);
|
|
63
|
+
line-height: 1.25;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
h3 {
|
|
67
|
+
font-size: var(--tai-text-lg);
|
|
68
|
+
line-height: 1.3;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
h4,
|
|
73
|
+
h5,
|
|
74
|
+
h6 {
|
|
75
|
+
font-size: var(--tai-text-md);
|
|
76
|
+
line-height: 1.4;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* A real selection pair, not the near-background accent TINT: the tint is the
|
|
81
|
+
fill behind a selected nav item, and over body copy it is 1.13:1 (light) /
|
|
82
|
+
1.11:1 (dark) against the unselected background — dragging over prose gives
|
|
83
|
+
essentially no feedback about what had been selected.
|
|
84
|
+
|
|
85
|
+
Both halves of the pair are declared, so the selected run's own contrast is
|
|
86
|
+
`on-accent` over `accent` and nothing else: derived from the token hex
|
|
87
|
+
values, 4.99:1 in light (#ffffff on #dc143c) and 5.36:1 in dark (#0c0e12 on
|
|
88
|
+
#ed4c67). Both clear the 4.5:1 body-text floor, and the accent ground is the
|
|
89
|
+
same 4.99:1 / 5.36:1 away from the page ground, so the selection is visible
|
|
90
|
+
as well as legible. */
|
|
91
|
+
::selection {
|
|
92
|
+
background: var(--tai-color-accent);
|
|
93
|
+
color: var(--tai-color-on-accent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Chromium paints its own opaque autofill background, which reads as a light
|
|
97
|
+
patch inside a dark form. Repaint it from the surface token and force the
|
|
98
|
+
text color to match. */
|
|
99
|
+
input:-webkit-autofill,
|
|
100
|
+
input:-webkit-autofill:hover,
|
|
101
|
+
input:-webkit-autofill:focus,
|
|
102
|
+
textarea:-webkit-autofill {
|
|
103
|
+
-webkit-text-fill-color: var(--tai-color-text);
|
|
104
|
+
-webkit-box-shadow: 0 0 0 100px var(--tai-color-surface-raised) inset;
|
|
105
|
+
caret-color: var(--tai-color-text);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* -- The uppercase mono label ---------------------------------------------
|
|
109
|
+
ONE owner for the 11 px tracked-mono label style: nav section headers,
|
|
110
|
+
table headers, page eyebrows, chart axis labels. Never re-derived inline.
|
|
111
|
+
`.tai-nav-section-header` is the shell's existing name for the same style;
|
|
112
|
+
`.tai-table th` is here because a header cell in a table this repo renders
|
|
113
|
+
cannot carry a class. Prose tables are NOT in this set: their header text is
|
|
114
|
+
authored elsewhere, and shrinking it below its own body cells (11 px against
|
|
115
|
+
13.5 px) would invert the table's hierarchy — `.tai-prose th` styles itself. */
|
|
116
|
+
.tai-label,
|
|
117
|
+
.tai-nav-section-header,
|
|
118
|
+
.tai-table th {
|
|
119
|
+
font-family: var(--tai-font-mono);
|
|
120
|
+
font-size: var(--tai-text-xs);
|
|
121
|
+
font-weight: 500;
|
|
122
|
+
line-height: 1.4;
|
|
123
|
+
letter-spacing: 0.13em;
|
|
124
|
+
text-transform: uppercase;
|
|
125
|
+
color: var(--tai-color-text-muted);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.tai-nav-section-header {
|
|
129
|
+
padding: var(--tai-space-4) var(--tai-space-3) var(--tai-space-2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* -- Shared focus ---------------------------------------------------------
|
|
133
|
+
An outline (not a box-shadow) at a 2 px offset: an outline follows the
|
|
134
|
+
element's `border-radius` and takes no part in layout, so the ring never
|
|
135
|
+
shifts what it surrounds.
|
|
136
|
+
|
|
137
|
+
It is NOT immune to clipping. An outline is painted in the element's own box
|
|
138
|
+
and is cut by an ancestor's overflow box exactly as a box-shadow is, so a
|
|
139
|
+
scrolling ancestor has to leave the ring room or it cuts every ring inside it
|
|
140
|
+
down to the two segments that fit (see `.tai-select-viewport`). */
|
|
141
|
+
.tai-btn:focus-visible,
|
|
142
|
+
.tai-icon-btn:focus-visible,
|
|
143
|
+
.tai-link:focus-visible,
|
|
144
|
+
.tai-input:focus-visible,
|
|
145
|
+
.tai-textarea:focus-visible,
|
|
146
|
+
.tai-select-trigger:focus-visible,
|
|
147
|
+
.tai-select-item:focus-visible,
|
|
148
|
+
.tai-checkbox:focus-visible,
|
|
149
|
+
.tai-radio:focus-visible,
|
|
150
|
+
.tai-segment:focus-visible,
|
|
151
|
+
.tai-chip:focus-visible,
|
|
152
|
+
.tai-tab:focus-visible,
|
|
153
|
+
.tai-nav-item:focus-visible,
|
|
154
|
+
.tai-nav-link:focus-visible,
|
|
155
|
+
/* The brand lockup is a link home in every shell that renders it — it declares
|
|
156
|
+
link affordances (`text-decoration: none` over the heading ink) and sits in
|
|
157
|
+
the sidebar's tab order ahead of the nav, so it needs the ring like any
|
|
158
|
+
other chrome link. */
|
|
159
|
+
.tai-brand:focus-visible,
|
|
160
|
+
.tai-skip-link:focus-visible,
|
|
161
|
+
.tai-card-interactive:focus-visible,
|
|
162
|
+
.tai-scroll-region:focus-visible,
|
|
163
|
+
.tai-code-block:focus-visible,
|
|
164
|
+
.tai-prose a:focus-visible,
|
|
165
|
+
.tai-prose pre:focus-visible,
|
|
166
|
+
.tai-drawer:focus-visible,
|
|
167
|
+
/* Radix hard-codes `tabIndex: 0` on every `TabsContent`, so a tab panel is a
|
|
168
|
+
tab stop whether or not it holds one — it needs the ring like any other. */
|
|
169
|
+
.tai-tabpanel:focus-visible,
|
|
170
|
+
.tai-dialog:focus-visible {
|
|
171
|
+
outline: 2px solid var(--tai-color-focus-ring);
|
|
172
|
+
outline-offset: 2px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* The input's own boundary is the focus indicator's partner: it strengthens
|
|
176
|
+
to the accent so the field reads as active even at 200 % zoom. */
|
|
177
|
+
.tai-input:focus-visible,
|
|
178
|
+
.tai-textarea:focus-visible,
|
|
179
|
+
.tai-select-trigger:focus-visible {
|
|
180
|
+
border-color: var(--tai-color-accent);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* -- Buttons --------------------------------------------------------------
|
|
184
|
+
One 36 px geometry for every control; the 36 px is a FLOOR and the label
|
|
185
|
+
sits on a line box inside it. Hover lifts 2 px; a coarse pointer RAISES that
|
|
186
|
+
floor to a 44 px target (the pointer-target band below wins on touch) and
|
|
187
|
+
leaves the lift alone — a lift is painted only while `:hover` holds, and the
|
|
188
|
+
three rules that do cancel it are `:disabled`, reduced motion, and `:active`.
|
|
189
|
+
|
|
190
|
+
The line box is 1.25, not 1. A caller's label may WRAP here — that is the
|
|
191
|
+
whole point of the `overflow-wrap` below — and a unitless line-height of 1
|
|
192
|
+
makes a line box SHORTER than the font's own content area, so consecutive
|
|
193
|
+
wrapped lines overlap. At 1.25 a single-line label still costs
|
|
194
|
+
13.5 x 1.25 + 16 padding + 2 border = 34.9 px, under the 36 px floor, so
|
|
195
|
+
nothing about the resting control moves. */
|
|
196
|
+
.tai-btn {
|
|
197
|
+
display: inline-flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
gap: var(--tai-space-2);
|
|
201
|
+
box-sizing: border-box;
|
|
202
|
+
min-height: var(--tai-control-height);
|
|
203
|
+
padding: var(--tai-space-2) var(--tai-space-4);
|
|
204
|
+
border: 1px solid transparent;
|
|
205
|
+
border-radius: var(--tai-radius-md);
|
|
206
|
+
font-family: var(--tai-font-sans);
|
|
207
|
+
font-size: var(--tai-text-md);
|
|
208
|
+
font-weight: 500;
|
|
209
|
+
line-height: 1.25;
|
|
210
|
+
text-decoration: none;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
/* A flex item's default `min-width: auto` refuses to shrink below its own
|
|
213
|
+
content, so caller text — a server label, a tag, an operator's own
|
|
214
|
+
string — pushes the document instead of breaking inside the control.
|
|
215
|
+
Measured at 320 px: without this the rule pushes a document scrollbar.
|
|
216
|
+
`flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
|
|
217
|
+
because the overflow is the control's own text, not its item order. */
|
|
218
|
+
min-width: 0;
|
|
219
|
+
overflow-wrap: anywhere;
|
|
220
|
+
transition:
|
|
221
|
+
background-color var(--tai-motion-fast) ease,
|
|
222
|
+
border-color var(--tai-motion-fast) ease,
|
|
223
|
+
color var(--tai-motion-fast) ease,
|
|
224
|
+
box-shadow var(--tai-motion-fast) ease,
|
|
225
|
+
transform var(--tai-motion-fast) ease;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.tai-btn:hover:not(:disabled):not([aria-disabled='true']) {
|
|
229
|
+
transform: translateY(-2px);
|
|
230
|
+
box-shadow: var(--tai-shadow-lift);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.tai-btn:active:not(:disabled):not([aria-disabled='true']) {
|
|
234
|
+
transform: translateY(0);
|
|
235
|
+
box-shadow: none;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.tai-btn:disabled,
|
|
239
|
+
.tai-btn[aria-disabled='true'] {
|
|
240
|
+
background: var(--tai-color-surface-disabled);
|
|
241
|
+
color: var(--tai-color-text-disabled);
|
|
242
|
+
border-color: var(--tai-color-border-disabled);
|
|
243
|
+
box-shadow: none;
|
|
244
|
+
transform: none;
|
|
245
|
+
cursor: not-allowed;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.tai-btn-primary {
|
|
249
|
+
background: var(--tai-color-accent);
|
|
250
|
+
border-color: var(--tai-color-accent);
|
|
251
|
+
color: var(--tai-color-on-accent);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.tai-btn-primary:hover:not(:disabled):not([aria-disabled='true']) {
|
|
255
|
+
background: var(--tai-color-accent-hover);
|
|
256
|
+
border-color: var(--tai-color-accent-hover);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.tai-btn-secondary {
|
|
260
|
+
background: var(--tai-color-surface-raised);
|
|
261
|
+
border-color: var(--tai-color-control-border);
|
|
262
|
+
color: var(--tai-color-text);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.tai-btn-secondary:hover:not(:disabled):not([aria-disabled='true']) {
|
|
266
|
+
background: var(--tai-color-surface);
|
|
267
|
+
border-color: var(--tai-color-text);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.tai-btn-ghost {
|
|
271
|
+
background: transparent;
|
|
272
|
+
border-color: transparent;
|
|
273
|
+
color: var(--tai-color-text-muted);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.tai-btn-ghost:hover:not(:disabled):not([aria-disabled='true']) {
|
|
277
|
+
background: var(--tai-color-surface);
|
|
278
|
+
color: var(--tai-color-text);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.tai-btn-danger {
|
|
282
|
+
background: var(--tai-color-danger);
|
|
283
|
+
border-color: var(--tai-color-danger);
|
|
284
|
+
color: var(--tai-color-danger-text);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.tai-btn-danger:hover:not(:disabled):not([aria-disabled='true']) {
|
|
288
|
+
background: var(--tai-color-danger-hover);
|
|
289
|
+
border-color: var(--tai-color-danger-hover);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* A square button whose only content is a 16 px icon; its accessible name
|
|
293
|
+
comes from the consumer's aria-label. */
|
|
294
|
+
.tai-icon-btn {
|
|
295
|
+
display: inline-flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
justify-content: center;
|
|
298
|
+
box-sizing: border-box;
|
|
299
|
+
width: var(--tai-control-height);
|
|
300
|
+
height: var(--tai-control-height);
|
|
301
|
+
padding: 0;
|
|
302
|
+
border: 1px solid transparent;
|
|
303
|
+
border-radius: var(--tai-radius-md);
|
|
304
|
+
background: transparent;
|
|
305
|
+
color: var(--tai-color-text-muted);
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
transition:
|
|
308
|
+
background-color var(--tai-motion-fast) ease,
|
|
309
|
+
color var(--tai-motion-fast) ease;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.tai-icon-btn:hover:not(:disabled) {
|
|
313
|
+
background: var(--tai-color-surface);
|
|
314
|
+
color: var(--tai-color-text);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.tai-icon-btn:disabled {
|
|
318
|
+
color: var(--tai-color-text-disabled);
|
|
319
|
+
cursor: not-allowed;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* -- Links ---------------------------------------------------------------- */
|
|
323
|
+
.tai-link {
|
|
324
|
+
color: var(--tai-color-accent);
|
|
325
|
+
text-decoration: underline;
|
|
326
|
+
text-underline-offset: 2px;
|
|
327
|
+
border-radius: var(--tai-radius-sm);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.tai-link:hover {
|
|
331
|
+
color: var(--tai-color-accent-hover);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* -- Icons ----------------------------------------------------------------
|
|
335
|
+
Rendered at 16 px in currentColor at full opacity; a 24-unit viewBox and a
|
|
336
|
+
1.6 stroke keep the whole set optically consistent. */
|
|
337
|
+
.tai-icon {
|
|
338
|
+
display: inline-block;
|
|
339
|
+
flex: none;
|
|
340
|
+
width: 16px;
|
|
341
|
+
height: 16px;
|
|
342
|
+
vertical-align: -0.15em;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* A mark inside a control keeps its size while the TEXT beside it takes the
|
|
346
|
+
shrinking. Every row flex below declares a `min-width`, which is what tells
|
|
347
|
+
the layout it may shrink past its own content; the text has
|
|
348
|
+
`overflow-wrap: anywhere` to survive that, an svg has nothing, so it is
|
|
349
|
+
squashed instead — measured at 320 px, a 16 px mark inside `.tai-status`
|
|
350
|
+
came out 11.34 px wide. `.tai-icon` already carries `flex: none`, so what
|
|
351
|
+
this reaches is the svg a CONSUMER brings. The group is reconciled against
|
|
352
|
+
the sheet's row-flex-with-a-min-width rules in both directions by
|
|
353
|
+
`sheet-contract.test.ts`, so a new one cannot be silently left out. */
|
|
354
|
+
.tai-badge > svg,
|
|
355
|
+
.tai-btn > svg,
|
|
356
|
+
.tai-chip > svg,
|
|
357
|
+
.tai-choice > svg,
|
|
358
|
+
.tai-drawer-header > svg,
|
|
359
|
+
.tai-error-state-title > svg,
|
|
360
|
+
.tai-field-error > svg,
|
|
361
|
+
.tai-nav-item > svg,
|
|
362
|
+
.tai-nav-link > svg,
|
|
363
|
+
.tai-segment > svg,
|
|
364
|
+
.tai-select-trigger > svg,
|
|
365
|
+
.tai-status > svg {
|
|
366
|
+
flex: none;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* -- Fields, inputs, selects ----------------------------------------------
|
|
370
|
+
`.tai-field-group` is the container a group Field renders around its
|
|
371
|
+
children so the group has a `role` to carry its own name and description. It
|
|
372
|
+
has to reproduce `.tai-field`'s formatting context exactly or wrapping would
|
|
373
|
+
not be layout-neutral — the children would become items of the wrapper's
|
|
374
|
+
default `block` layout and lose the field's gap — so it is the SAME rule
|
|
375
|
+
rather than a copy that can drift. A copy carrying its own `min-width: 0`
|
|
376
|
+
was a measured no-op: the automatic minimum size applies on the MAIN axis,
|
|
377
|
+
and in a column flex container that is the block axis. */
|
|
378
|
+
.tai-field,
|
|
379
|
+
.tai-field-group {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex-direction: column;
|
|
382
|
+
gap: var(--tai-space-2);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.tai-field-label {
|
|
386
|
+
font-family: var(--tai-font-sans);
|
|
387
|
+
font-size: var(--tai-text-sm);
|
|
388
|
+
font-weight: 600;
|
|
389
|
+
line-height: 1.4;
|
|
390
|
+
color: var(--tai-color-text);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.tai-field-hint {
|
|
394
|
+
font-family: var(--tai-font-sans);
|
|
395
|
+
font-size: var(--tai-text-sm);
|
|
396
|
+
line-height: 1.5;
|
|
397
|
+
color: var(--tai-color-text-muted);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Icon beside a message nobody here writes — a validation string, a schema
|
|
401
|
+
path, a browser exception. It therefore WRAPS and breaks inside a long token
|
|
402
|
+
rather than pushing the document sideways at 320 px, and the icon aligns to
|
|
403
|
+
the first line instead of floating in the middle of a wrapped block. */
|
|
404
|
+
.tai-field-error {
|
|
405
|
+
display: flex;
|
|
406
|
+
align-items: flex-start;
|
|
407
|
+
flex-wrap: wrap;
|
|
408
|
+
gap: var(--tai-space-2);
|
|
409
|
+
min-width: 0;
|
|
410
|
+
overflow-wrap: anywhere;
|
|
411
|
+
font-family: var(--tai-font-sans);
|
|
412
|
+
font-size: var(--tai-text-sm);
|
|
413
|
+
line-height: 1.5;
|
|
414
|
+
color: var(--tai-color-err-text);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.tai-input,
|
|
418
|
+
.tai-textarea,
|
|
419
|
+
.tai-select-trigger {
|
|
420
|
+
box-sizing: border-box;
|
|
421
|
+
width: 100%;
|
|
422
|
+
background: var(--tai-color-surface-raised);
|
|
423
|
+
color: var(--tai-color-text);
|
|
424
|
+
border: 1px solid var(--tai-color-control-border);
|
|
425
|
+
border-radius: var(--tai-radius-md);
|
|
426
|
+
font-family: var(--tai-font-sans);
|
|
427
|
+
font-size: var(--tai-text-md);
|
|
428
|
+
transition:
|
|
429
|
+
border-color var(--tai-motion-fast) ease,
|
|
430
|
+
background-color var(--tai-motion-fast) ease;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.tai-input,
|
|
434
|
+
.tai-select-trigger {
|
|
435
|
+
/* A FLOOR, not a fixed box — the same shape `.tai-btn` uses. The text inside
|
|
436
|
+
is authored in rem so a viewer's font-size preference scales it; a fixed
|
|
437
|
+
36 px box with 8 px block padding and a 1px border leaves that text 18 px
|
|
438
|
+
to live in, so at a large root size a field would clip while the button
|
|
439
|
+
beside it grew. */
|
|
440
|
+
min-height: var(--tai-control-height);
|
|
441
|
+
/* The 8 × 16 control padding a button wears, so a field and the button
|
|
442
|
+
beside it start their text on the same inset. */
|
|
443
|
+
padding: var(--tai-space-2) var(--tai-space-4);
|
|
444
|
+
/* The button's line box, for the same reason: the trigger's chosen value is
|
|
445
|
+
caller data and breaks anywhere, and a line box shorter than the font's
|
|
446
|
+
content area overlaps the line under it. */
|
|
447
|
+
line-height: 1.25;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* The trigger lays out a label beside its chevron; a text input has no
|
|
451
|
+
children to lay out, so the flex box belongs here alone. */
|
|
452
|
+
.tai-select-trigger {
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: var(--tai-space-2);
|
|
456
|
+
justify-content: space-between;
|
|
457
|
+
text-align: left;
|
|
458
|
+
cursor: pointer;
|
|
459
|
+
/* `width: 100%` sizes the BOX; it says nothing about what is inside it. The
|
|
460
|
+
`<Select.Value>` span still reports its own min-content, and the chosen
|
|
461
|
+
value is caller data — a server id, a plugin tag — so an unbroken one
|
|
462
|
+
pushes the DOCUMENT sideways out of the field it sits in. Measured
|
|
463
|
+
headless: a 35 / 41 / 44-character value pushed a document scrollbar at
|
|
464
|
+
320 / 360 / 390 px, and with these two declarations no length does. The
|
|
465
|
+
span is a flex item of this row, so it needs the same pair every other
|
|
466
|
+
caller-sized control carries. */
|
|
467
|
+
min-width: 0;
|
|
468
|
+
overflow-wrap: anywhere;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.tai-textarea {
|
|
472
|
+
display: block;
|
|
473
|
+
min-height: calc(var(--tai-control-height) * 2);
|
|
474
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
475
|
+
line-height: 1.5;
|
|
476
|
+
resize: vertical;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.tai-input::placeholder,
|
|
480
|
+
.tai-textarea::placeholder {
|
|
481
|
+
color: var(--tai-color-placeholder);
|
|
482
|
+
opacity: 1;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Hover excludes the invalid state rather than sitting above it: `:not()`
|
|
486
|
+
carries its argument's specificity, so an unscoped hover rule would outrank
|
|
487
|
+
the attribute rule below and drop the error border on pointer-over.
|
|
488
|
+
|
|
489
|
+
It excludes the FOCUSED state for the same arithmetic and the opposite
|
|
490
|
+
reason. That `:not()` escalation put hover at (0,4,0) against
|
|
491
|
+
`:focus-visible`'s (0,2,0), so resting the pointer on the field the user is
|
|
492
|
+
typing in reverted the accent partner-border — the indicator declared
|
|
493
|
+
above as "the focus indicator's partner" — to the plain text colour. */
|
|
494
|
+
.tai-input:not([aria-invalid='true']):hover:not(:disabled):not(:focus-visible),
|
|
495
|
+
.tai-textarea:not([aria-invalid='true']):hover:not(:disabled):not(:focus-visible),
|
|
496
|
+
.tai-select-trigger:not([aria-invalid='true']):hover:not(:disabled):not(:focus-visible) {
|
|
497
|
+
border-color: var(--tai-color-text);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.tai-input:disabled,
|
|
501
|
+
.tai-textarea:disabled,
|
|
502
|
+
.tai-select-trigger:disabled {
|
|
503
|
+
background: var(--tai-color-surface-disabled);
|
|
504
|
+
color: var(--tai-color-text-disabled);
|
|
505
|
+
border-color: var(--tai-color-border-disabled);
|
|
506
|
+
cursor: not-allowed;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* An invalid field is announced by its message and icon; the border is a
|
|
510
|
+
supporting cue, never the only one.
|
|
511
|
+
|
|
512
|
+
DISABLED WINS over invalid, and the exclusion is written rather than left to
|
|
513
|
+
source order: a control the operator cannot act on has to READ as one, and
|
|
514
|
+
the message and icon carry the invalidity either way. That is the same
|
|
515
|
+
ordering `.tai-checkbox:disabled` takes over its own checked ground, for the
|
|
516
|
+
same reason. Without the `:not()` these two rules are both (0,2,0) and the
|
|
517
|
+
later one simply wins, which put an error boundary on a field nobody can
|
|
518
|
+
correct. */
|
|
519
|
+
.tai-input[aria-invalid='true']:not(:disabled),
|
|
520
|
+
.tai-textarea[aria-invalid='true']:not(:disabled),
|
|
521
|
+
.tai-select-trigger[aria-invalid='true']:not(:disabled) {
|
|
522
|
+
border-color: var(--tai-color-err-text);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.tai-input-mono,
|
|
526
|
+
.tai-textarea-mono {
|
|
527
|
+
font-family: var(--tai-font-mono);
|
|
528
|
+
font-size: var(--tai-text-code);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* The listbox is portalled to the document body, so it is a SIBLING of any
|
|
532
|
+
open dialog rather than a descendant — hence the popover layer, which
|
|
533
|
+
clears the dialog scrim that would otherwise cover and swallow it. The
|
|
534
|
+
height cap yields to the space the popper actually has, so a long list in a
|
|
535
|
+
short viewport still scrolls inside the screen; the fallback covers the
|
|
536
|
+
in-flow suggestion list, which wears these classes without a popper to
|
|
537
|
+
publish that measurement. */
|
|
538
|
+
.tai-select-content {
|
|
539
|
+
z-index: var(--tai-z-popover);
|
|
540
|
+
/* Capped against the VIEWPORT as well as its own content, like every other
|
|
541
|
+
floating surface: the width is set by the longest option label, which is
|
|
542
|
+
caller data and can exceed a 320 px screen on its own. */
|
|
543
|
+
max-width: min(20rem, calc(100vw - 2 * var(--tai-space-4)));
|
|
544
|
+
max-height: min(18rem, var(--radix-select-content-available-height, 60vh));
|
|
545
|
+
overflow-y: auto;
|
|
546
|
+
/* The cap is on the BORDER box, or it is not a cap at all: with the default
|
|
547
|
+
`content-box` the side padding and the 1 px boundary sit outside it. Radix
|
|
548
|
+
inline-styles `box-sizing: border-box` on its own popper, but the in-flow
|
|
549
|
+
suggestion list wears this class as a plain `<ul>` with no Radix near it. */
|
|
550
|
+
box-sizing: border-box;
|
|
551
|
+
background: var(--tai-color-surface-raised);
|
|
552
|
+
/* The popover's fill IS the page ground in light (`surface-raised` and `bg`
|
|
553
|
+
are both #ffffff, 1.000:1), so this edge is the whole of what says where
|
|
554
|
+
the surface begins — and a boundary that identifies a component is held to
|
|
555
|
+
3:1 (WCAG 1.4.11). The decorative tier gives 1.47:1; the control tier gives
|
|
556
|
+
4.21:1 light / 3.65:1 dark. */
|
|
557
|
+
border: 1px solid var(--tai-color-control-border);
|
|
558
|
+
border-radius: var(--tai-radius-md);
|
|
559
|
+
box-shadow: var(--tai-shadow-overlay);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/* The padding lives on the OVERFLOW box, not on its parent. Radix inline-styles
|
|
563
|
+
the viewport `overflow: hidden auto`, and the shared focus rule paints
|
|
564
|
+
`outline: 2px solid …; outline-offset: 2px` — 4 px outside the option's
|
|
565
|
+
border box. With the padding one level up, the viewport clipped the left and
|
|
566
|
+
right segments of every option's ring and the top or bottom segment of the
|
|
567
|
+
first and last, so a keyboard user saw two horizontal bars instead of a
|
|
568
|
+
ring. */
|
|
569
|
+
.tai-select-viewport {
|
|
570
|
+
/* 8 px against a 4 px ring reach (a 2 px outline at a 2 px offset), so the
|
|
571
|
+
gap is the ring PLUS a spacing step rather than exactly the ring. At 4 px
|
|
572
|
+
the two numbers are equal and every one of them — the outline width, the
|
|
573
|
+
offset, a scrollbar the viewport grows when the list is long — clips the
|
|
574
|
+
ring the moment it moves by a pixel, with no gate able to see it. The gate
|
|
575
|
+
reads both numbers off this sheet and holds the padding above the reach. */
|
|
576
|
+
padding: var(--tai-space-2);
|
|
577
|
+
/* Radix moves the keyboard highlight with `scrollIntoView({block:'nearest'})`,
|
|
578
|
+
which aligns to the PADDING box — so from the eighth option on, the scroll
|
|
579
|
+
consumes exactly what the padding above reserves and every step loses its
|
|
580
|
+
leading ring edge. `scroll-padding` keeps that gap on the scroll port too.
|
|
581
|
+
Measured over a 52-step keyboard walk: 36 steps clipped without it, 0 with
|
|
582
|
+
it. It matches the padding, so the two cannot drift apart. */
|
|
583
|
+
scroll-padding: var(--tai-space-2);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.tai-select-item {
|
|
587
|
+
display: flex;
|
|
588
|
+
align-items: center;
|
|
589
|
+
/* The option text sits left; the chosen-value mark sits at the far edge. */
|
|
590
|
+
justify-content: space-between;
|
|
591
|
+
gap: var(--tai-space-2);
|
|
592
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
593
|
+
border-radius: var(--tai-radius-sm);
|
|
594
|
+
font-family: var(--tai-font-sans);
|
|
595
|
+
font-size: var(--tai-text-md);
|
|
596
|
+
line-height: 1.4;
|
|
597
|
+
color: var(--tai-color-text);
|
|
598
|
+
cursor: pointer;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/* A group name is supplied by the caller — a plugin's own tag, not a string
|
|
602
|
+
this design system authored — so it keeps the label geometry but not the
|
|
603
|
+
uppercasing, which would rewrite a case-carrying name (`gRPC`, `iOS`). */
|
|
604
|
+
.tai-select-group-label {
|
|
605
|
+
padding: var(--tai-space-2) var(--tai-space-3) var(--tai-space-1);
|
|
606
|
+
text-transform: none;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* Radix moves DOM focus to the highlighted Select.Item during keyboard and
|
|
610
|
+
pointer navigation, which stamps `data-highlighted`. The completion list
|
|
611
|
+
wears these same classes on its own buttons, which Radix never touches, so
|
|
612
|
+
pointer hover is named alongside it — the accent tint reads as the current
|
|
613
|
+
option in both themes (WCAG 2.4.7). */
|
|
614
|
+
.tai-select-item:not([data-disabled]):hover,
|
|
615
|
+
.tai-select-item[data-highlighted] {
|
|
616
|
+
background: var(--tai-color-accent-tint);
|
|
617
|
+
color: var(--tai-color-accent-on-tint);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* Radix stamps `data-highlighted` for pointer hover as well as for keyboard
|
|
621
|
+
movement, and it moves DOM focus either way — so the highlight alone cannot
|
|
622
|
+
say which one happened. `:focus-visible` can, and the shared ring above is
|
|
623
|
+
already keyed on it, so the only state left to suppress is the UA ring a
|
|
624
|
+
browser may still draw on plain `:focus`. Guarding this on
|
|
625
|
+
`:not(:focus-visible)` is what keeps the keyboard ring: without it this rule
|
|
626
|
+
matches the keyboard case too, at equal specificity and later in the sheet,
|
|
627
|
+
and every Select loses its visible focus indicator (WCAG 2.4.7). */
|
|
628
|
+
.tai-select-item[data-highlighted]:not(:focus-visible) {
|
|
629
|
+
outline: none;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* The highlight is only WHERE the reader is; it moves as they arrow through
|
|
633
|
+
the list. The chosen value needs a mark of its own, or it becomes
|
|
634
|
+
unidentifiable the moment any other option is highlighted. The mark is a
|
|
635
|
+
glyph rather than a color so it survives the highlight's own repaint, and it
|
|
636
|
+
renders only for the chosen option, so nothing shifts as the value moves. */
|
|
637
|
+
.tai-select-item-indicator {
|
|
638
|
+
display: flex;
|
|
639
|
+
flex: none;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.tai-select-item[data-disabled] {
|
|
643
|
+
color: var(--tai-color-text-disabled);
|
|
644
|
+
cursor: not-allowed;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/* -- Checkbox + radio ------------------------------------------------------ */
|
|
648
|
+
.tai-checkbox,
|
|
649
|
+
.tai-radio {
|
|
650
|
+
position: relative;
|
|
651
|
+
display: inline-flex;
|
|
652
|
+
align-items: center;
|
|
653
|
+
justify-content: center;
|
|
654
|
+
flex: none;
|
|
655
|
+
width: 1rem;
|
|
656
|
+
height: 1rem;
|
|
657
|
+
padding: 0;
|
|
658
|
+
background: var(--tai-color-surface-raised);
|
|
659
|
+
border: 1px solid var(--tai-color-control-border);
|
|
660
|
+
color: var(--tai-color-on-accent);
|
|
661
|
+
cursor: pointer;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/* 16 px is the VISUAL size of the box. The pointer target is grown past it by
|
|
665
|
+
a transparent overlay so a box with no adjacent label — a table's selection
|
|
666
|
+
column — still clears the 24 px minimum (WCAG 2.5.8) without the row growing
|
|
667
|
+
around it. */
|
|
668
|
+
.tai-checkbox::after,
|
|
669
|
+
.tai-radio::after {
|
|
670
|
+
content: '';
|
|
671
|
+
position: absolute;
|
|
672
|
+
inset: -4px;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.tai-checkbox {
|
|
676
|
+
border-radius: var(--tai-radius-sm);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.tai-radio {
|
|
680
|
+
border-radius: var(--tai-radius-full);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.tai-checkbox[data-state='checked'],
|
|
684
|
+
.tai-checkbox[data-state='indeterminate'] {
|
|
685
|
+
background: var(--tai-color-accent);
|
|
686
|
+
border-color: var(--tai-color-accent);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/* HOVER. The box strengthens its boundary to the text ink — the same move
|
|
690
|
+
`.tai-input` makes, and the reason both are legible over the raised ground
|
|
691
|
+
they sit on (17.7:1 light, 13.9:1 dark). A `.tai-choice` label is the box's
|
|
692
|
+
larger hit target, so hovering the label is hovering the control and moves
|
|
693
|
+
the box with it; the label's own ink is already the strongest body ink there
|
|
694
|
+
is, so there is nothing above it for the label itself to move to. */
|
|
695
|
+
.tai-checkbox:hover:not(:disabled),
|
|
696
|
+
.tai-radio:hover:not(:disabled),
|
|
697
|
+
.tai-choice:hover > .tai-checkbox:not(:disabled),
|
|
698
|
+
.tai-choice:hover > .tai-radio:not(:disabled) {
|
|
699
|
+
border-color: var(--tai-color-text);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* A checked box keeps its accent GROUND and moves to the hover member of the
|
|
703
|
+
ramp instead. `:not()` carries its argument's specificity, so the rule above
|
|
704
|
+
sits at (0,3,0) / (0,4,0) and would otherwise repaint the checked boundary
|
|
705
|
+
the plain text colour while the fill stayed accent; these selectors match
|
|
706
|
+
its arithmetic and come after it. */
|
|
707
|
+
.tai-checkbox[data-state='checked']:hover:not(:disabled),
|
|
708
|
+
.tai-checkbox[data-state='indeterminate']:hover:not(:disabled),
|
|
709
|
+
.tai-choice:hover > .tai-checkbox[data-state='checked']:not(:disabled),
|
|
710
|
+
.tai-choice:hover > .tai-checkbox[data-state='indeterminate']:not(:disabled) {
|
|
711
|
+
background: var(--tai-color-accent-hover);
|
|
712
|
+
border-color: var(--tai-color-accent-hover);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.tai-radio-indicator {
|
|
716
|
+
display: block;
|
|
717
|
+
width: 0.5rem;
|
|
718
|
+
height: 0.5rem;
|
|
719
|
+
border-radius: var(--tai-radius-full);
|
|
720
|
+
background: var(--tai-color-accent);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* The tick is drawn in `currentColor`, so the disabled ground has to bring its
|
|
724
|
+
own foreground with it: repainting the box without one left a checked AND
|
|
725
|
+
disabled control showing white on white. A read-only pane is exactly where
|
|
726
|
+
the checked state must still READ, so this is the same disabled foreground
|
|
727
|
+
every other control uses rather than a hidden one. `.tai-radio-indicator`
|
|
728
|
+
paints its own background and is unaffected. */
|
|
729
|
+
.tai-checkbox:disabled,
|
|
730
|
+
.tai-radio:disabled {
|
|
731
|
+
background: var(--tai-color-surface-disabled);
|
|
732
|
+
color: var(--tai-color-text-disabled);
|
|
733
|
+
border-color: var(--tai-color-border-disabled);
|
|
734
|
+
cursor: not-allowed;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.tai-choice {
|
|
738
|
+
display: inline-flex;
|
|
739
|
+
align-items: center;
|
|
740
|
+
gap: var(--tai-space-2);
|
|
741
|
+
min-height: var(--tai-control-height);
|
|
742
|
+
font-family: var(--tai-font-sans);
|
|
743
|
+
font-size: var(--tai-text-md);
|
|
744
|
+
line-height: 1.4;
|
|
745
|
+
color: var(--tai-color-text);
|
|
746
|
+
cursor: pointer;
|
|
747
|
+
/* A flex item's default `min-width: auto` refuses to shrink below its own
|
|
748
|
+
content, so caller text — a server label, a tag, an operator's own
|
|
749
|
+
string — pushes the document instead of breaking inside the control.
|
|
750
|
+
Measured at 320 px: without this the rule pushes a document scrollbar.
|
|
751
|
+
`flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
|
|
752
|
+
because the overflow is the control's own text, not its item order. */
|
|
753
|
+
min-width: 0;
|
|
754
|
+
overflow-wrap: anywhere;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* -- Segmented control -----------------------------------------------------
|
|
758
|
+
A compact radiogroup whose options ARE their content: no dot indicator, an
|
|
759
|
+
icon plus a visually-hidden label. Vertical is the rail's layout, where a
|
|
760
|
+
horizontal trio cannot fit in 72 px. */
|
|
761
|
+
.tai-segmented {
|
|
762
|
+
display: inline-flex;
|
|
763
|
+
flex-wrap: wrap;
|
|
764
|
+
gap: var(--tai-space-1);
|
|
765
|
+
padding: var(--tai-space-1);
|
|
766
|
+
background: var(--tai-color-surface);
|
|
767
|
+
border: 1px solid var(--tai-color-border);
|
|
768
|
+
border-radius: var(--tai-radius-md);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.tai-segmented[data-orientation='vertical'] {
|
|
772
|
+
flex-direction: column;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.tai-segment {
|
|
776
|
+
display: inline-flex;
|
|
777
|
+
align-items: center;
|
|
778
|
+
justify-content: center;
|
|
779
|
+
gap: var(--tai-space-2);
|
|
780
|
+
box-sizing: border-box;
|
|
781
|
+
/* A hit-target FLOOR (WCAG 2.5.8), not a content floor: 28 px is a small
|
|
782
|
+
fraction of the narrowest viewport, so the option can still shrink far
|
|
783
|
+
below its own label — which is the shape the 320 px contract asks for,
|
|
784
|
+
and the reason this rule keeps its `min-width` rather than dropping to
|
|
785
|
+
`0`. What the label needs on top of it is permission to BREAK.
|
|
786
|
+
`visuallyHiddenLabel` is opt-in PER OPTION, so the API default paints the
|
|
787
|
+
caller's own string beside the icon; measured headless, an unbroken one
|
|
788
|
+
pushed a document scrollbar from 43 / 48 / 53 characters at
|
|
789
|
+
320 / 360 / 390 px, and with `overflow-wrap` no length does. */
|
|
790
|
+
min-width: 28px;
|
|
791
|
+
min-height: 28px;
|
|
792
|
+
overflow-wrap: anywhere;
|
|
793
|
+
padding: var(--tai-space-1) var(--tai-space-2);
|
|
794
|
+
border: 1px solid transparent;
|
|
795
|
+
border-radius: var(--tai-radius-sm);
|
|
796
|
+
background: transparent;
|
|
797
|
+
color: var(--tai-color-text-muted);
|
|
798
|
+
font-family: var(--tai-font-sans);
|
|
799
|
+
font-size: var(--tai-text-sm);
|
|
800
|
+
font-weight: 500;
|
|
801
|
+
line-height: 1.25;
|
|
802
|
+
cursor: pointer;
|
|
803
|
+
transition:
|
|
804
|
+
background-color var(--tai-motion-fast) ease,
|
|
805
|
+
color var(--tai-motion-fast) ease;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.tai-segment:not([data-state='checked']):hover:not(:disabled) {
|
|
809
|
+
background: var(--tai-color-surface-raised);
|
|
810
|
+
color: var(--tai-color-text);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.tai-segment[data-state='checked'] {
|
|
814
|
+
background: var(--tai-color-accent-tint);
|
|
815
|
+
border-color: var(--tai-color-accent);
|
|
816
|
+
color: var(--tai-color-accent-on-tint);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* The selected segment KEEPS its tint on hover — it is the only cue an
|
|
820
|
+
icon-only option has — and strengthens its border instead. The border is the
|
|
821
|
+
part that moves in BOTH themes: in dark the accent-hover ink is the same
|
|
822
|
+
value as accent-on-tint, so a color change there would be invisible. */
|
|
823
|
+
.tai-segment[data-state='checked']:hover:not(:disabled) {
|
|
824
|
+
border-color: var(--tai-color-accent-hover);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.tai-segment:disabled {
|
|
828
|
+
color: var(--tai-color-text-disabled);
|
|
829
|
+
cursor: not-allowed;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/* -- Cards ----------------------------------------------------------------- */
|
|
833
|
+
.tai-card {
|
|
834
|
+
box-sizing: border-box;
|
|
835
|
+
background: var(--tai-color-surface-raised);
|
|
836
|
+
border: 1px solid var(--tai-color-border);
|
|
837
|
+
border-radius: var(--tai-radius-lg);
|
|
838
|
+
padding: var(--tai-space-5);
|
|
839
|
+
color: var(--tai-color-text);
|
|
840
|
+
transition:
|
|
841
|
+
border-color var(--tai-motion-fast) ease,
|
|
842
|
+
box-shadow var(--tai-motion-base) ease,
|
|
843
|
+
transform var(--tai-motion-base) ease;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.tai-card-interactive:hover,
|
|
847
|
+
.tai-card-interactive:focus-within {
|
|
848
|
+
transform: translateY(-4px);
|
|
849
|
+
border-color: var(--tai-color-border-strong);
|
|
850
|
+
box-shadow: var(--tai-shadow-lift);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.tai-card-title {
|
|
854
|
+
margin: 0;
|
|
855
|
+
font-size: var(--tai-text-lg);
|
|
856
|
+
font-weight: 600;
|
|
857
|
+
line-height: 1.3;
|
|
858
|
+
color: var(--tai-color-heading);
|
|
859
|
+
letter-spacing: -0.03em;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/* -- Tabs ------------------------------------------------------------------ */
|
|
863
|
+
.tai-tablist {
|
|
864
|
+
display: flex;
|
|
865
|
+
/* Tab labels are caller data and a plugin adds its own, so the strip wraps
|
|
866
|
+
rather than pushing the DOCUMENT sideways — the five Settings tabs alone
|
|
867
|
+
need 394 px and there is no horizontal scroll to reach them at 320. Every
|
|
868
|
+
strip in this sheet that lays CALLER-SIZED content out in a row wraps for
|
|
869
|
+
the same reason; `narrow-viewport.test.ts` reconciles that set against the
|
|
870
|
+
sheet in both directions, so a new one cannot be silently exempted. */
|
|
871
|
+
flex-wrap: wrap;
|
|
872
|
+
gap: var(--tai-space-1);
|
|
873
|
+
border-bottom: 1px solid var(--tai-color-border);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.tai-tab {
|
|
877
|
+
appearance: none;
|
|
878
|
+
box-sizing: border-box;
|
|
879
|
+
min-height: var(--tai-control-height);
|
|
880
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
881
|
+
background: transparent;
|
|
882
|
+
border: none;
|
|
883
|
+
border-bottom: 2px solid transparent;
|
|
884
|
+
font-family: var(--tai-font-sans);
|
|
885
|
+
font-size: var(--tai-text-md);
|
|
886
|
+
font-weight: 500;
|
|
887
|
+
line-height: 1.25;
|
|
888
|
+
color: var(--tai-color-text-muted);
|
|
889
|
+
cursor: pointer;
|
|
890
|
+
transition:
|
|
891
|
+
color var(--tai-motion-fast) ease,
|
|
892
|
+
border-color var(--tai-motion-fast) ease;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.tai-tab:hover:not(:disabled) {
|
|
896
|
+
color: var(--tai-color-text);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.tai-tab[data-state='active'] {
|
|
900
|
+
color: var(--tai-color-text);
|
|
901
|
+
border-bottom-color: var(--tai-color-accent);
|
|
902
|
+
font-weight: 600;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.tai-tab:disabled {
|
|
906
|
+
color: var(--tai-color-text-disabled);
|
|
907
|
+
cursor: not-allowed;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.tai-tabpanel {
|
|
911
|
+
padding: var(--tai-space-4) 0;
|
|
912
|
+
color: var(--tai-color-text);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/* -- Navigation ------------------------------------------------------------
|
|
916
|
+
The current item carries three cues at once: an accent tint, accent-on-tint
|
|
917
|
+
text, and a 3 px inset rail — plus `aria-current="page"` for assistive tech.
|
|
918
|
+
`.tai-nav-link` is the shell's existing name for the same item. */
|
|
919
|
+
.tai-nav-item,
|
|
920
|
+
.tai-nav-link {
|
|
921
|
+
display: flex;
|
|
922
|
+
align-items: center;
|
|
923
|
+
gap: var(--tai-space-2);
|
|
924
|
+
box-sizing: border-box;
|
|
925
|
+
/* A flex item's default `min-width: auto` refuses to shrink below its own
|
|
926
|
+
content, so caller text — a server label, a tag, an operator's own
|
|
927
|
+
string — pushes the document instead of breaking inside the control.
|
|
928
|
+
Measured at 320 px: without this the rule pushes a document scrollbar.
|
|
929
|
+
`flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
|
|
930
|
+
because the overflow is the control's own text, not its item order. */
|
|
931
|
+
min-width: 0;
|
|
932
|
+
overflow-wrap: anywhere;
|
|
933
|
+
min-height: var(--tai-control-height);
|
|
934
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
935
|
+
border-radius: var(--tai-radius-md);
|
|
936
|
+
font-family: var(--tai-font-sans);
|
|
937
|
+
font-size: var(--tai-text-md);
|
|
938
|
+
font-weight: 500;
|
|
939
|
+
line-height: 1.25;
|
|
940
|
+
color: var(--tai-color-text);
|
|
941
|
+
text-decoration: none;
|
|
942
|
+
transition:
|
|
943
|
+
background-color var(--tai-motion-fast) ease,
|
|
944
|
+
color var(--tai-motion-fast) ease;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* A nav item lives in CHROME, not on the page ground, and chrome is painted
|
|
948
|
+
from `--tai-color-surface` — `.tai-shell-sidebar` below is, and so is any
|
|
949
|
+
host sidebar that follows it. Stepping the hover to that same tone therefore
|
|
950
|
+
resolved to 1.000:1 against the ground the item was sitting on: hovering a
|
|
951
|
+
nav item changed nothing at all. The line tone is the one near-ground token
|
|
952
|
+
that is distinct from every ground this sheet paints, derived statically
|
|
953
|
+
from the token hex values: against `surface` 1.41:1 light / 1.78:1 dark,
|
|
954
|
+
against `bg` 1.47:1 / 1.88:1, against `surface-raised` 1.47:1 / 1.66:1. The
|
|
955
|
+
item's own ink stays well clear of it (12.0:1 light, 8.4:1 dark). */
|
|
956
|
+
.tai-nav-item:hover,
|
|
957
|
+
.tai-nav-link:hover {
|
|
958
|
+
background: var(--tai-color-border-strong);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.tai-nav-item[aria-current='page'],
|
|
962
|
+
.tai-nav-link[aria-current='page'] {
|
|
963
|
+
background: var(--tai-color-accent-tint);
|
|
964
|
+
color: var(--tai-color-accent-on-tint);
|
|
965
|
+
font-weight: 600;
|
|
966
|
+
box-shadow: inset 3px 0 0 0 var(--tai-color-accent);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/* The current row answers the pointer too. `[aria-current='page']` and `:hover`
|
|
970
|
+
are both (0,2,0), so the rule above wins by ORDER and the one row a reader is
|
|
971
|
+
standing on is the only row in the rail that does not respond.
|
|
972
|
+
|
|
973
|
+
It cannot take the line tone the other rows take. The tint is translucent, so
|
|
974
|
+
that tone shows through it and carries `accent-on-tint` down to 3.79:1 light /
|
|
975
|
+
3.33:1 dark, under the 4.5:1 the label itself needs (WCAG 1.4.3) — the ground
|
|
976
|
+
step is bounded by the ink sitting on it. The tint is DOUBLED instead: a
|
|
977
|
+
second layer of the same token over the first, which deepens the ground to
|
|
978
|
+
1.13:1 light / 1.16:1 dark against the resting row while the label still
|
|
979
|
+
clears its floor at 4.67:1 / 5.05:1. All four figures are derived statically
|
|
980
|
+
from the token values. */
|
|
981
|
+
.tai-nav-item[aria-current='page']:hover,
|
|
982
|
+
.tai-nav-link[aria-current='page']:hover {
|
|
983
|
+
background-image: linear-gradient(var(--tai-color-accent-tint), var(--tai-color-accent-tint));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/* Skip-to-content: off-screen until focused, so a keyboard or screen-reader
|
|
987
|
+
user can bypass the sidebar nav and jump straight to the page content
|
|
988
|
+
(WCAG 2.4.1). It is the first focusable element in the shell. */
|
|
989
|
+
.tai-skip-link {
|
|
990
|
+
position: absolute;
|
|
991
|
+
left: var(--tai-space-2);
|
|
992
|
+
top: -4rem;
|
|
993
|
+
z-index: var(--tai-z-tooltip);
|
|
994
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
995
|
+
border-radius: var(--tai-radius-md);
|
|
996
|
+
background: var(--tai-color-accent);
|
|
997
|
+
color: var(--tai-color-on-accent);
|
|
998
|
+
font-family: var(--tai-font-sans);
|
|
999
|
+
font-size: var(--tai-text-md);
|
|
1000
|
+
line-height: 1.4;
|
|
1001
|
+
text-decoration: none;
|
|
1002
|
+
transition: top var(--tai-motion-fast) ease-in;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
.tai-skip-link:focus {
|
|
1006
|
+
top: var(--tai-space-2);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/* -- Chips ------------------------------------------------------------------
|
|
1010
|
+
`.tai-chip` publishes a CONTROL: a pointer, a hover, a pressed ground, a
|
|
1011
|
+
disabled rendering and a 44 px coarse target, for a consumer's filter or
|
|
1012
|
+
toggle chip. Every chip this repo renders is a TAG and opts out of all of it
|
|
1013
|
+
with `.tai-chip-static`, so the interactive half is reachable only from
|
|
1014
|
+
outside — which is a published-surface affordance, not dead code, and
|
|
1015
|
+
`class-contract.test.ts` records it as one by name. */
|
|
1016
|
+
.tai-chip {
|
|
1017
|
+
display: inline-flex;
|
|
1018
|
+
align-items: center;
|
|
1019
|
+
gap: var(--tai-space-1);
|
|
1020
|
+
box-sizing: border-box;
|
|
1021
|
+
min-height: 24px;
|
|
1022
|
+
padding: var(--tai-space-1) var(--tai-space-3);
|
|
1023
|
+
border: 1px solid var(--tai-color-control-border);
|
|
1024
|
+
border-radius: var(--tai-radius-full);
|
|
1025
|
+
background: var(--tai-color-surface-raised);
|
|
1026
|
+
color: var(--tai-color-text);
|
|
1027
|
+
font-family: var(--tai-font-sans);
|
|
1028
|
+
font-size: var(--tai-text-sm);
|
|
1029
|
+
line-height: 1.25;
|
|
1030
|
+
cursor: pointer;
|
|
1031
|
+
/* A flex item's default `min-width: auto` refuses to shrink below its own
|
|
1032
|
+
content, so caller text — a server label, a tag, an operator's own
|
|
1033
|
+
string — pushes the document instead of breaking inside the control.
|
|
1034
|
+
Measured at 320 px: without this the rule pushes a document scrollbar.
|
|
1035
|
+
`flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
|
|
1036
|
+
because the overflow is the control's own text, not its item order. */
|
|
1037
|
+
min-width: 0;
|
|
1038
|
+
overflow-wrap: anywhere;
|
|
1039
|
+
transition:
|
|
1040
|
+
background-color var(--tai-motion-fast) ease,
|
|
1041
|
+
border-color var(--tai-motion-fast) ease,
|
|
1042
|
+
color var(--tai-motion-fast) ease;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.tai-chip:not(.tai-chip-static):not([aria-pressed='true']):hover:not(:disabled) {
|
|
1046
|
+
background: var(--tai-color-surface);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.tai-chip[aria-pressed='true'] {
|
|
1050
|
+
background: var(--tai-color-accent-tint);
|
|
1051
|
+
border-color: var(--tai-color-accent);
|
|
1052
|
+
color: var(--tai-color-accent-on-tint);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/* A selected chip KEEPS its tint on hover and strengthens its border instead —
|
|
1056
|
+
the one part that moves in both themes (see the segment rule above). */
|
|
1057
|
+
.tai-chip[aria-pressed='true']:hover:not(:disabled) {
|
|
1058
|
+
border-color: var(--tai-color-accent-hover);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.tai-chip:disabled {
|
|
1062
|
+
color: var(--tai-color-text-disabled);
|
|
1063
|
+
border-color: var(--tai-color-border-disabled);
|
|
1064
|
+
cursor: not-allowed;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/* A chip that only reads (a tag, a "+N more" count) is not a control, so it
|
|
1068
|
+
shows no pointer and is excluded from the hover rule above. */
|
|
1069
|
+
.tai-chip-static {
|
|
1070
|
+
cursor: default;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/* -- Badges -----------------------------------------------------------------
|
|
1074
|
+
A badge is tint + its own `-text` color. The solid variants are for chart
|
|
1075
|
+
legends and status fills, and always carry the ink label. */
|
|
1076
|
+
.tai-badge {
|
|
1077
|
+
display: inline-flex;
|
|
1078
|
+
align-items: center;
|
|
1079
|
+
gap: var(--tai-space-1);
|
|
1080
|
+
box-sizing: border-box;
|
|
1081
|
+
min-height: 20px;
|
|
1082
|
+
padding: var(--tai-space-1) var(--tai-space-2);
|
|
1083
|
+
border-radius: var(--tai-radius-full);
|
|
1084
|
+
background: var(--tai-color-accent-tint);
|
|
1085
|
+
color: var(--tai-color-accent-on-tint);
|
|
1086
|
+
font-family: var(--tai-font-sans);
|
|
1087
|
+
font-size: var(--tai-text-xs);
|
|
1088
|
+
font-weight: 500;
|
|
1089
|
+
line-height: 1.4;
|
|
1090
|
+
/* A flex item's default `min-width: auto` refuses to shrink below its own
|
|
1091
|
+
content, so caller text — a server label, a tag, an operator's own
|
|
1092
|
+
string — pushes the document instead of breaking inside the control.
|
|
1093
|
+
Measured at 320 px: without this the rule pushes a document scrollbar.
|
|
1094
|
+
`flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
|
|
1095
|
+
because the overflow is the control's own text, not its item order. */
|
|
1096
|
+
min-width: 0;
|
|
1097
|
+
overflow-wrap: anywhere;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.tai-badge-neutral {
|
|
1101
|
+
background: var(--tai-color-surface);
|
|
1102
|
+
color: var(--tai-color-text-muted);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.tai-badge-ok {
|
|
1106
|
+
background: var(--tai-color-ok-tint);
|
|
1107
|
+
color: var(--tai-color-ok-text);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.tai-badge-err {
|
|
1111
|
+
background: var(--tai-color-err-tint);
|
|
1112
|
+
color: var(--tai-color-err-text);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
.tai-badge-warn {
|
|
1116
|
+
background: var(--tai-color-warn-tint);
|
|
1117
|
+
color: var(--tai-color-warn-text);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.tai-badge-solid-ok {
|
|
1121
|
+
background: var(--tai-color-ok-fill);
|
|
1122
|
+
color: var(--tai-color-on-fill);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
.tai-badge-solid-err {
|
|
1126
|
+
background: var(--tai-color-err-fill);
|
|
1127
|
+
color: var(--tai-color-on-fill);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.tai-badge-solid-warn {
|
|
1131
|
+
background: var(--tai-color-warn-fill);
|
|
1132
|
+
color: var(--tai-color-on-fill);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/* -- Status ----------------------------------------------------------------
|
|
1136
|
+
Status is always an icon plus a label; the color only reinforces it. */
|
|
1137
|
+
.tai-status {
|
|
1138
|
+
display: inline-flex;
|
|
1139
|
+
/* `flex-start`, not `center`: once the label wraps, a centred mark floats
|
|
1140
|
+
between the two lines instead of sitting on the first one — the exact
|
|
1141
|
+
reason `.tai-field-error` gives for its own `flex-start`. Measured at
|
|
1142
|
+
320/360/375/390 px: icon offset 11 px -> 0, box height unchanged.
|
|
1143
|
+
It moves the SINGLE-line case too, by design and not only as a side
|
|
1144
|
+
effect: a 13.5 px label on a 1.4 line box is an 18.9 px line, so a 16 px
|
|
1145
|
+
mark centred in it sat 1.45 px below the line's top edge and now sits on
|
|
1146
|
+
it. That is the alignment the wrapped case needs, and one line of text is
|
|
1147
|
+
the wrapped case with one line. */
|
|
1148
|
+
align-items: flex-start;
|
|
1149
|
+
gap: var(--tai-space-2);
|
|
1150
|
+
font-family: var(--tai-font-sans);
|
|
1151
|
+
font-size: var(--tai-text-md);
|
|
1152
|
+
line-height: 1.4;
|
|
1153
|
+
/* A flex item's default `min-width: auto` refuses to shrink below its own
|
|
1154
|
+
content, so caller text — a server label, a tag, an operator's own
|
|
1155
|
+
string — pushes the document instead of breaking inside the control.
|
|
1156
|
+
Measured at 320 px: without this the rule pushes a document scrollbar.
|
|
1157
|
+
`flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
|
|
1158
|
+
because the overflow is the control's own text, not its item order. */
|
|
1159
|
+
min-width: 0;
|
|
1160
|
+
overflow-wrap: anywhere;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.tai-status-ok {
|
|
1164
|
+
color: var(--tai-color-ok-text);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.tai-status-err {
|
|
1168
|
+
color: var(--tai-color-err-text);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.tai-status-warn {
|
|
1172
|
+
color: var(--tai-color-warn-text);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.tai-status-pending {
|
|
1176
|
+
color: var(--tai-color-text-muted);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/* -- Tables -----------------------------------------------------------------
|
|
1180
|
+
Mono uppercase headers, tinted row hover, numerics right-aligned on
|
|
1181
|
+
tabular figures so columns line up. */
|
|
1182
|
+
/* A table cell is not a flex item. Auto table layout distributes width from
|
|
1183
|
+
each column's MIN-CONTENT, and `overflow-wrap: anywhere` — unlike
|
|
1184
|
+
`break-word` — is taken into account when that min-content is computed. So a
|
|
1185
|
+
column holding one of these controls reported a min-content of ONE CHARACTER
|
|
1186
|
+
and the label was laid out a letter per line: measured, a live 5-column
|
|
1187
|
+
table rendered its "Revoke" button 6 lines / 99 px at 320-390 px and still 2
|
|
1188
|
+
lines / 45 px at 768 px, which is not a narrow viewport at all. Because
|
|
1189
|
+
`.tai-table` is `width: 100%`, this also fired INSIDE a `.tai-scroll-region`:
|
|
1190
|
+
the table stopped wanting to be wider than the box, so the region's
|
|
1191
|
+
`overflow-x: auto` never engaged and the control was mangled instead of the
|
|
1192
|
+
table scrolling. Inside a table these controls therefore keep their
|
|
1193
|
+
intrinsic minimum; the 320 px contract outside tables is untouched.
|
|
1194
|
+
|
|
1195
|
+
The group is EVERY caller-sized item, not only the ones a cell holds today.
|
|
1196
|
+
Live table call sites exist for four of them — `.tai-btn` in the actions
|
|
1197
|
+
column of seven screens, `.tai-badge` in eleven tables, `.tai-chip` via
|
|
1198
|
+
TagChips, `.tai-status` in VersionHistoryPanel. The rest are GUARD
|
|
1199
|
+
declarations, and this comment is not claiming a nav item or a select is
|
|
1200
|
+
rendered in a cell: they are not. That is deliberate rather than sloppy. A
|
|
1201
|
+
rule matching nothing costs one line here and nothing at runtime, whereas a
|
|
1202
|
+
hand-audited "these ones cannot appear in a table" list costs a live,
|
|
1203
|
+
user-visible regression on the day someone adds the call site — and this
|
|
1204
|
+
contract has now been bitten by exactly that twice. `narrow-viewport.test.ts`
|
|
1205
|
+
reconciles this group against its caller-sized list as a set equality in
|
|
1206
|
+
BOTH directions, so adding an item forces both rules or the gate reddens. */
|
|
1207
|
+
.tai-table .tai-btn,
|
|
1208
|
+
.tai-table .tai-badge,
|
|
1209
|
+
.tai-table .tai-chip,
|
|
1210
|
+
.tai-table .tai-status,
|
|
1211
|
+
.tai-table .tai-choice,
|
|
1212
|
+
.tai-table .tai-nav-item,
|
|
1213
|
+
.tai-table .tai-nav-link,
|
|
1214
|
+
.tai-table .tai-select-trigger {
|
|
1215
|
+
min-width: auto;
|
|
1216
|
+
overflow-wrap: normal;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/* `.tai-segment` takes the wrap half ONLY. The eight above carry
|
|
1220
|
+
`min-width: 0`, so `auto` is what hands them an intrinsic minimum back;
|
|
1221
|
+
this one carries a 28 px / 44 px POINTER TARGET (WCAG 2.5.8), and `auto` on
|
|
1222
|
+
a flex item is the CONTENT-based minimum — for the icon-only option this
|
|
1223
|
+
control is designed around that is roughly 26 px, i.e. below the target. Its
|
|
1224
|
+
own floor already IS the non-zero minimum the rule above is restoring, so
|
|
1225
|
+
restoring it would only lower it. */
|
|
1226
|
+
.tai-table .tai-segment {
|
|
1227
|
+
overflow-wrap: normal;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.tai-table {
|
|
1231
|
+
width: 100%;
|
|
1232
|
+
border-collapse: collapse;
|
|
1233
|
+
font-family: var(--tai-font-sans);
|
|
1234
|
+
font-size: var(--tai-text-md);
|
|
1235
|
+
line-height: 1.5;
|
|
1236
|
+
color: var(--tai-color-text);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.tai-table th {
|
|
1240
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
1241
|
+
text-align: left;
|
|
1242
|
+
vertical-align: bottom;
|
|
1243
|
+
border-bottom: 1px solid var(--tai-color-border);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.tai-table td {
|
|
1247
|
+
padding: var(--tai-space-3);
|
|
1248
|
+
vertical-align: top;
|
|
1249
|
+
border-bottom: 1px solid var(--tai-color-border);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/* The row hover is a LOCATOR; a diff row's tint is MEANING, and meaning keeps
|
|
1253
|
+
the ground — the same call `.tai-segment[data-state='checked']:hover` makes
|
|
1254
|
+
for the one cue an icon-only option has. Written as an exclusion because the
|
|
1255
|
+
arithmetic goes the other way on its own: `.tai-table tbody tr:hover` is
|
|
1256
|
+
(0,1,2) and `.tai-diff-added` is (0,1,0), so every hovered diff row lost its
|
|
1257
|
+
colour to the accent tint. `:not()` carries its argument's specificity, so
|
|
1258
|
+
naming the three here is also what lifts this rule clear of them. The gate
|
|
1259
|
+
reconciles this list against the sheet's own semantic row grounds. */
|
|
1260
|
+
.tai-table tbody tr:not(.tai-diff-added):not(.tai-diff-removed):not(.tai-diff-changed):hover {
|
|
1261
|
+
background: var(--tai-color-accent-tint);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
.tai-table td[data-numeric='true'],
|
|
1265
|
+
.tai-table th[data-numeric='true'] {
|
|
1266
|
+
text-align: right;
|
|
1267
|
+
font-variant-numeric: tabular-nums;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
.tai-table-id {
|
|
1271
|
+
font-family: var(--tai-font-mono);
|
|
1272
|
+
font-size: var(--tai-text-code);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/* -- Scroll regions ---------------------------------------------------------
|
|
1276
|
+
Horizontal overflow scrolls inside its own container. The tab stop, the
|
|
1277
|
+
`role="region"`, and the label are applied by `ScrollRegion` while the
|
|
1278
|
+
container overflows, or while it still holds focus — a table that fits and
|
|
1279
|
+
has never been focused is not a tab stop. */
|
|
1280
|
+
.tai-scroll-region {
|
|
1281
|
+
overflow-x: auto;
|
|
1282
|
+
max-width: 100%;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/* -- Surfaces: dialog, drawer, tooltip --------------------------------------- */
|
|
1286
|
+
.tai-overlay {
|
|
1287
|
+
position: fixed;
|
|
1288
|
+
inset: 0;
|
|
1289
|
+
z-index: var(--tai-z-overlay);
|
|
1290
|
+
background: var(--tai-color-scrim);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.tai-dialog {
|
|
1294
|
+
position: fixed;
|
|
1295
|
+
top: 50%;
|
|
1296
|
+
left: 50%;
|
|
1297
|
+
z-index: var(--tai-z-dialog);
|
|
1298
|
+
transform: translate(-50%, -50%);
|
|
1299
|
+
box-sizing: border-box;
|
|
1300
|
+
width: min(560px, calc(100vw - 2 * var(--tai-space-4)));
|
|
1301
|
+
/* `vh` is the LARGE viewport: on a phone with a retracting browser chrome the
|
|
1302
|
+
cap is taller than the screen by the height of that chrome, so a tall dialog
|
|
1303
|
+
runs its own actions row under the URL bar — and it is a MODAL, so there is
|
|
1304
|
+
no page behind it to scroll the bar away with. `dvh` is the live value. The
|
|
1305
|
+
pair is the fallback, not a preference: `dvh` arrived after this package's
|
|
1306
|
+
floor (Chrome 108, Firefox 101), and a browser below it drops the second
|
|
1307
|
+
declaration and keeps the first. */
|
|
1308
|
+
max-height: calc(100vh - 2 * var(--tai-space-6));
|
|
1309
|
+
max-height: calc(100dvh - 2 * var(--tai-space-6));
|
|
1310
|
+
overflow-y: auto;
|
|
1311
|
+
padding: var(--tai-space-6);
|
|
1312
|
+
background: var(--tai-color-surface-raised);
|
|
1313
|
+
border: 1px solid var(--tai-color-border);
|
|
1314
|
+
border-radius: var(--tai-radius-overlay);
|
|
1315
|
+
box-shadow: var(--tai-shadow-overlay);
|
|
1316
|
+
color: var(--tai-color-text);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.tai-dialog-title {
|
|
1320
|
+
margin: 0 0 var(--tai-space-2);
|
|
1321
|
+
font-size: var(--tai-text-section);
|
|
1322
|
+
line-height: 1.25;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.tai-dialog-actions {
|
|
1326
|
+
display: flex;
|
|
1327
|
+
/* `flex-end` sends any overflow LEFT, where LTR scrolling never reaches, and
|
|
1328
|
+
the confirm label is caller data. Wrapping keeps every action on screen. */
|
|
1329
|
+
flex-wrap: wrap;
|
|
1330
|
+
justify-content: flex-end;
|
|
1331
|
+
gap: var(--tai-space-2);
|
|
1332
|
+
margin-top: var(--tai-space-5);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/* A side-anchored, full-height modal panel, published for a host to mount and
|
|
1336
|
+
drive itself. Radix Dialog supplies the trap, the scroll lock, Esc, the
|
|
1337
|
+
inert background, and focus return. */
|
|
1338
|
+
.tai-drawer {
|
|
1339
|
+
position: fixed;
|
|
1340
|
+
top: 0;
|
|
1341
|
+
bottom: 0;
|
|
1342
|
+
left: 0;
|
|
1343
|
+
z-index: var(--tai-z-dialog);
|
|
1344
|
+
box-sizing: border-box;
|
|
1345
|
+
display: flex;
|
|
1346
|
+
flex-direction: column;
|
|
1347
|
+
gap: var(--tai-space-3);
|
|
1348
|
+
width: min(280px, calc(100vw - var(--tai-space-8)));
|
|
1349
|
+
overflow-y: auto;
|
|
1350
|
+
padding: var(--tai-space-4);
|
|
1351
|
+
background: var(--tai-color-surface-raised);
|
|
1352
|
+
border-right: 1px solid var(--tai-color-border);
|
|
1353
|
+
box-shadow: var(--tai-shadow-overlay);
|
|
1354
|
+
color: var(--tai-color-text);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.tai-drawer[data-side='right'] {
|
|
1358
|
+
left: auto;
|
|
1359
|
+
right: 0;
|
|
1360
|
+
border-right: none;
|
|
1361
|
+
border-left: 1px solid var(--tai-color-border);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/* Caller-supplied title beside a close control: it wraps rather than pushing
|
|
1365
|
+
the drawer's own scroller sideways. */
|
|
1366
|
+
.tai-drawer-header {
|
|
1367
|
+
display: flex;
|
|
1368
|
+
align-items: center;
|
|
1369
|
+
justify-content: space-between;
|
|
1370
|
+
flex-wrap: wrap;
|
|
1371
|
+
gap: var(--tai-space-2);
|
|
1372
|
+
min-width: 0;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.tai-tooltip {
|
|
1376
|
+
z-index: var(--tai-z-tooltip);
|
|
1377
|
+
/* The cap is on the BORDER box, or it is not a cap at all: with the default
|
|
1378
|
+
`content-box` the 12 px side padding and the 1 px boundary sit outside it,
|
|
1379
|
+
so at 320 px the 288 px cap painted a 314 px tip and pushed the document
|
|
1380
|
+
— for any consumer whose host has no preflight to set `box-sizing`
|
|
1381
|
+
globally. Every other capped surface in this sheet already says so. */
|
|
1382
|
+
box-sizing: border-box;
|
|
1383
|
+
max-width: min(20rem, calc(100vw - 2 * var(--tai-space-4)));
|
|
1384
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
1385
|
+
background: var(--tai-color-surface-raised);
|
|
1386
|
+
/* The tip's fill IS the page ground in light (`surface-raised` and `bg` are
|
|
1387
|
+
both #ffffff, 1.000:1) and it has no scrim under it, so this edge is the
|
|
1388
|
+
whole of what says where the tip begins — a boundary that identifies a
|
|
1389
|
+
component, held to 3:1 (WCAG 1.4.11). The decorative tier gives 1.47:1; the
|
|
1390
|
+
control tier gives 4.21:1 light / 3.65:1 dark. */
|
|
1391
|
+
border: 1px solid var(--tai-color-control-border);
|
|
1392
|
+
border-radius: var(--tai-radius-md);
|
|
1393
|
+
box-shadow: var(--tai-shadow-overlay);
|
|
1394
|
+
font-family: var(--tai-font-sans);
|
|
1395
|
+
font-size: var(--tai-text-sm);
|
|
1396
|
+
line-height: 1.5;
|
|
1397
|
+
color: var(--tai-color-text);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/* -- Code, JSON, terminal surfaces -------------------------------------------- */
|
|
1401
|
+
.tai-code {
|
|
1402
|
+
padding: 0.1em 0.35em;
|
|
1403
|
+
background: var(--tai-color-code-bg);
|
|
1404
|
+
border-radius: var(--tai-radius-code);
|
|
1405
|
+
font-family: var(--tai-font-mono);
|
|
1406
|
+
font-size: var(--tai-text-code);
|
|
1407
|
+
color: var(--tai-color-text);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/* ONE code surface, under two names. `.tai-prose pre` is the same block for
|
|
1411
|
+
markdown this Studio did not author, so it is the same rule: a copy of these
|
|
1412
|
+
declarations beside it had already drifted — a `tab-size` of 8 against 2
|
|
1413
|
+
rendered the identical listing at four times the indent, and without
|
|
1414
|
+
`box-sizing` the padding and boundary sat outside the block's own width. */
|
|
1415
|
+
.tai-code-block,
|
|
1416
|
+
.tai-prose pre {
|
|
1417
|
+
box-sizing: border-box;
|
|
1418
|
+
padding: var(--tai-space-4);
|
|
1419
|
+
overflow-x: auto;
|
|
1420
|
+
background: var(--tai-color-code-bg);
|
|
1421
|
+
border: 1px solid var(--tai-color-border);
|
|
1422
|
+
border-radius: var(--tai-radius-md);
|
|
1423
|
+
font-family: var(--tai-font-mono);
|
|
1424
|
+
font-size: var(--tai-text-code);
|
|
1425
|
+
line-height: 1.75;
|
|
1426
|
+
color: var(--tai-color-text);
|
|
1427
|
+
white-space: pre;
|
|
1428
|
+
tab-size: 2;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/* The margin is the ONE declaration the two names do not share. A standalone
|
|
1432
|
+
block is placed by whatever stack it sits in, so it brings none of its own;
|
|
1433
|
+
a `<pre>` in prose is placed by `.tai-prose > * + *`, and a descendant
|
|
1434
|
+
selector outranks that rule, so zeroing it here would delete the prose
|
|
1435
|
+
rhythm above every code listing a README carries. */
|
|
1436
|
+
.tai-code-block {
|
|
1437
|
+
margin: 0;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/* A code surface holding a message rather than a listing: it WRAPS instead of
|
|
1441
|
+
scrolling, so a long line stays readable without becoming a scroll container
|
|
1442
|
+
that a keyboard would have to reach. */
|
|
1443
|
+
.tai-code-block-wrap {
|
|
1444
|
+
overflow-x: visible;
|
|
1445
|
+
white-space: pre-wrap;
|
|
1446
|
+
overflow-wrap: anywhere;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
.tai-syntax-key {
|
|
1450
|
+
color: var(--tai-color-syntax-key);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.tai-syntax-string {
|
|
1454
|
+
color: var(--tai-color-syntax-string);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.tai-syntax-number {
|
|
1458
|
+
color: var(--tai-color-syntax-number);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
.tai-syntax-bool {
|
|
1462
|
+
color: var(--tai-color-syntax-bool);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
.tai-syntax-null {
|
|
1466
|
+
color: var(--tai-color-text-muted);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
.tai-diff-added {
|
|
1470
|
+
background: var(--tai-color-ok-tint);
|
|
1471
|
+
color: var(--tai-color-ok-text);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
.tai-diff-removed {
|
|
1475
|
+
background: var(--tai-color-err-tint);
|
|
1476
|
+
color: var(--tai-color-err-text);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.tai-diff-changed {
|
|
1480
|
+
background: var(--tai-color-warn-tint);
|
|
1481
|
+
color: var(--tai-color-warn-text);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/* -- Feedback surfaces --------------------------------------------------------- */
|
|
1485
|
+
/* The block rests at the LINE tone and sweeps the raised tone across it. That
|
|
1486
|
+
resting tone is also what the reduced-motion rule below leaves behind, so
|
|
1487
|
+
the moving and the parked skeleton are the same block — one with a band
|
|
1488
|
+
travelling over it. Basing it on `surface` instead put the whole block a
|
|
1489
|
+
step from the page ground (1.05:1 light, 1.07:1 dark — no visible band at
|
|
1490
|
+
all, and dimmer than the parked state it was meant to enrich); on the line
|
|
1491
|
+
tone the band reads at 1.24:1 light and 1.22:1 dark, and the block itself at
|
|
1492
|
+
1.24:1 / 1.38:1 against the ground it sits on.
|
|
1493
|
+
`overflow: hidden` is what clips the band to the block's rounded corners. */
|
|
1494
|
+
.tai-skeleton {
|
|
1495
|
+
position: relative;
|
|
1496
|
+
overflow: hidden;
|
|
1497
|
+
border-radius: var(--tai-radius-sm);
|
|
1498
|
+
background: var(--tai-color-border);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/* The band rides on an overlay rather than on the block's own
|
|
1502
|
+
`background-position`: a background-position animation restyles and repaints
|
|
1503
|
+
the block on the main thread every frame, for every skeleton on the page,
|
|
1504
|
+
while a transform on an absolutely-positioned overlay is composited off it.
|
|
1505
|
+
Both ends of the gradient are fully transparent, so what shows through at the
|
|
1506
|
+
band's edges is the block's own resting tone — the sweep adds a streak rather
|
|
1507
|
+
than fading the block into the page. */
|
|
1508
|
+
.tai-skeleton::after {
|
|
1509
|
+
content: '';
|
|
1510
|
+
position: absolute;
|
|
1511
|
+
inset: 0;
|
|
1512
|
+
background: linear-gradient(
|
|
1513
|
+
90deg,
|
|
1514
|
+
transparent 0%,
|
|
1515
|
+
var(--tai-color-surface-raised) 50%,
|
|
1516
|
+
transparent 100%
|
|
1517
|
+
);
|
|
1518
|
+
animation: tai-shimmer 1.4s linear infinite;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/* A busy indicator is a non-text UI component: its RING is the whole of what
|
|
1522
|
+
identifies it, so the ring is held to 3:1 (WCAG 1.4.11) and cannot sit in
|
|
1523
|
+
the decorative line tier. Derived statically from the token hex values, the
|
|
1524
|
+
decorative `border-strong` gave 1.47:1 light / 1.88:1 dark on the page
|
|
1525
|
+
ground and 1.47:1 / 1.66:1 on a raised one; the contrast-safe control tier
|
|
1526
|
+
gives 4.21:1 / 3.65:1 on the page ground, 4.21:1 / 3.23:1 raised, 4.03:1 /
|
|
1527
|
+
3.45:1 on `surface` and 3.82:1 / 3.51:1 on the code ground. The leading arc
|
|
1528
|
+
is the accent, at 4.99:1 / 5.36:1 — the same argument `.tai-progress-track`
|
|
1529
|
+
makes for its own boundary. */
|
|
1530
|
+
.tai-spinner {
|
|
1531
|
+
display: inline-block;
|
|
1532
|
+
box-sizing: border-box;
|
|
1533
|
+
width: 16px;
|
|
1534
|
+
height: 16px;
|
|
1535
|
+
border: 2px solid var(--tai-color-control-border);
|
|
1536
|
+
border-top-color: var(--tai-color-accent);
|
|
1537
|
+
border-radius: var(--tai-radius-full);
|
|
1538
|
+
animation: tai-spin 0.7s linear infinite;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/* The track's ground is barely off the page ground, so its BOUNDARY is what
|
|
1542
|
+
makes the bar visible at all — and a progress bar's outline identifies the
|
|
1543
|
+
component, which rules out the decorative border tier. */
|
|
1544
|
+
.tai-progress-track {
|
|
1545
|
+
position: relative;
|
|
1546
|
+
overflow: hidden;
|
|
1547
|
+
box-sizing: border-box;
|
|
1548
|
+
width: 100%;
|
|
1549
|
+
height: 8px;
|
|
1550
|
+
background: var(--tai-color-surface);
|
|
1551
|
+
border: 1px solid var(--tai-color-control-border);
|
|
1552
|
+
border-radius: var(--tai-radius-full);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
.tai-progress-fill {
|
|
1556
|
+
height: 100%;
|
|
1557
|
+
background: var(--tai-color-accent);
|
|
1558
|
+
border-radius: var(--tai-radius-full);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
.tai-progress-fill-indeterminate {
|
|
1562
|
+
width: 30%;
|
|
1563
|
+
animation: tai-progress-indeterminate 1.2s ease-in-out infinite;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
.tai-empty-state {
|
|
1567
|
+
padding: var(--tai-space-8);
|
|
1568
|
+
text-align: center;
|
|
1569
|
+
font-family: var(--tai-font-sans);
|
|
1570
|
+
font-size: var(--tai-text-md);
|
|
1571
|
+
line-height: 1.5;
|
|
1572
|
+
color: var(--tai-color-text-muted);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.tai-empty-state-title {
|
|
1576
|
+
margin: 0;
|
|
1577
|
+
font-size: var(--tai-text-section);
|
|
1578
|
+
line-height: 1.25;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/* A server error is arbitrary text: a URL, a token, a base64 body. Without a
|
|
1582
|
+
break rule an unbroken run pushes the page sideways and the tail of the only
|
|
1583
|
+
message the operator has goes off-screen. */
|
|
1584
|
+
.tai-error-state,
|
|
1585
|
+
.tai-warn-state {
|
|
1586
|
+
overflow-wrap: anywhere;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.tai-error-state {
|
|
1590
|
+
padding: var(--tai-space-4);
|
|
1591
|
+
background: var(--tai-color-err-tint);
|
|
1592
|
+
border: 1px solid var(--tai-color-err-text);
|
|
1593
|
+
border-radius: var(--tai-radius-md);
|
|
1594
|
+
font-family: var(--tai-font-sans);
|
|
1595
|
+
font-size: var(--tai-text-md);
|
|
1596
|
+
line-height: 1.5;
|
|
1597
|
+
color: var(--tai-color-text);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
.tai-error-state-title {
|
|
1601
|
+
display: flex;
|
|
1602
|
+
align-items: center;
|
|
1603
|
+
flex-wrap: wrap;
|
|
1604
|
+
gap: var(--tai-space-2);
|
|
1605
|
+
min-width: 0;
|
|
1606
|
+
color: var(--tai-color-err-text);
|
|
1607
|
+
font-weight: 600;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/* The same panel one tone down, for a state that degraded rather than failed. */
|
|
1611
|
+
.tai-warn-state {
|
|
1612
|
+
padding: var(--tai-space-4);
|
|
1613
|
+
background: var(--tai-color-warn-tint);
|
|
1614
|
+
border: 1px solid var(--tai-color-warn-text);
|
|
1615
|
+
border-radius: var(--tai-radius-md);
|
|
1616
|
+
font-family: var(--tai-font-sans);
|
|
1617
|
+
font-size: var(--tai-text-md);
|
|
1618
|
+
line-height: 1.5;
|
|
1619
|
+
color: var(--tai-color-text);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/* -- Prose --------------------------------------------------------------------
|
|
1623
|
+
The style for HTML the Studio did not author: a registry README injected
|
|
1624
|
+
into a plugin's detail page. The elements a CommonMark + GFM renderer emits
|
|
1625
|
+
have rules here, and the measure is held to a readable line length. GFM's
|
|
1626
|
+
strikethrough (`del`) and task-list checkboxes are NOT styled: they inherit,
|
|
1627
|
+
and a task list additionally shows the `ul` disc beside its checkbox. */
|
|
1628
|
+
.tai-prose {
|
|
1629
|
+
font-family: var(--tai-font-sans);
|
|
1630
|
+
font-size: var(--tai-text-md);
|
|
1631
|
+
line-height: 1.7;
|
|
1632
|
+
color: var(--tai-color-text);
|
|
1633
|
+
overflow-wrap: break-word;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
.tai-prose > * + * {
|
|
1637
|
+
margin-top: var(--tai-space-4);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
.tai-prose p,
|
|
1641
|
+
.tai-prose ul,
|
|
1642
|
+
.tai-prose ol,
|
|
1643
|
+
.tai-prose blockquote {
|
|
1644
|
+
max-width: 68ch;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
.tai-prose h1,
|
|
1648
|
+
.tai-prose h2,
|
|
1649
|
+
.tai-prose h3,
|
|
1650
|
+
.tai-prose h4,
|
|
1651
|
+
.tai-prose h5,
|
|
1652
|
+
.tai-prose h6 {
|
|
1653
|
+
margin: var(--tai-space-6) 0 var(--tai-space-2);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
.tai-prose h1 {
|
|
1657
|
+
font-size: var(--tai-text-xl);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
.tai-prose h2 {
|
|
1661
|
+
font-size: var(--tai-text-section);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
.tai-prose h3 {
|
|
1665
|
+
font-size: var(--tai-text-lg);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/* A prose link is always underlined, so color is a reinforcement rather than
|
|
1669
|
+
the only cue. The two prose-link tokens carry the per-theme values (the dark
|
|
1670
|
+
rest state is the LIGHTER crimson, and hover moves to the other member of
|
|
1671
|
+
the ramp in both themes) — see the token file. */
|
|
1672
|
+
.tai-prose a {
|
|
1673
|
+
color: var(--tai-color-prose-link);
|
|
1674
|
+
text-decoration: underline;
|
|
1675
|
+
text-underline-offset: 2px;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
.tai-prose a:hover {
|
|
1679
|
+
color: var(--tai-color-prose-link-hover);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
.tai-prose ul,
|
|
1683
|
+
.tai-prose ol {
|
|
1684
|
+
padding-left: var(--tai-space-6);
|
|
1685
|
+
list-style-position: outside;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.tai-prose ul {
|
|
1689
|
+
list-style-type: disc;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
.tai-prose ol {
|
|
1693
|
+
list-style-type: decimal;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
.tai-prose li + li {
|
|
1697
|
+
margin-top: var(--tai-space-1);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
.tai-prose code {
|
|
1701
|
+
padding: 0.1em 0.35em;
|
|
1702
|
+
background: var(--tai-color-code-bg);
|
|
1703
|
+
border-radius: var(--tai-radius-code);
|
|
1704
|
+
font-family: var(--tai-font-mono);
|
|
1705
|
+
font-size: var(--tai-text-code);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
.tai-prose pre code {
|
|
1709
|
+
padding: 0;
|
|
1710
|
+
background: none;
|
|
1711
|
+
font-size: inherit;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
.tai-prose blockquote {
|
|
1715
|
+
padding-left: var(--tai-space-4);
|
|
1716
|
+
border-left: 3px solid var(--tai-color-border-strong);
|
|
1717
|
+
color: var(--tai-color-text-muted);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
.tai-prose hr {
|
|
1721
|
+
border: none;
|
|
1722
|
+
border-top: 1px solid var(--tai-color-border);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
.tai-prose img {
|
|
1726
|
+
max-width: 100%;
|
|
1727
|
+
height: auto;
|
|
1728
|
+
border-radius: var(--tai-radius-md);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
.tai-prose table {
|
|
1732
|
+
width: 100%;
|
|
1733
|
+
border-collapse: collapse;
|
|
1734
|
+
font-size: var(--tai-text-md);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/* A markdown header cell is content, not chrome: it keeps the body's size and
|
|
1738
|
+
the author's own casing, and reads as a header through weight and ink. */
|
|
1739
|
+
.tai-prose th {
|
|
1740
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
1741
|
+
text-align: left;
|
|
1742
|
+
font-weight: 600;
|
|
1743
|
+
color: var(--tai-color-heading);
|
|
1744
|
+
border-bottom: 1px solid var(--tai-color-border-strong);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
.tai-prose td {
|
|
1748
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
1749
|
+
border-bottom: 1px solid var(--tai-color-border);
|
|
1750
|
+
vertical-align: top;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/* -- Page composition ----------------------------------------------------------
|
|
1754
|
+
`.tai-page` is the screen wrapper, `.tai-stack` the vertical rhythm every
|
|
1755
|
+
screen was hand-rolling as a flex column. */
|
|
1756
|
+
.tai-page {
|
|
1757
|
+
box-sizing: border-box;
|
|
1758
|
+
display: flex;
|
|
1759
|
+
flex-direction: column;
|
|
1760
|
+
gap: var(--tai-space-6);
|
|
1761
|
+
width: 100%;
|
|
1762
|
+
max-width: 1440px;
|
|
1763
|
+
margin: 0 auto;
|
|
1764
|
+
padding: var(--tai-space-6) var(--tai-space-8);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
.tai-stack {
|
|
1768
|
+
display: flex;
|
|
1769
|
+
flex-direction: column;
|
|
1770
|
+
gap: var(--tai-space-4);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
.tai-stack-2 {
|
|
1774
|
+
gap: var(--tai-space-2);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.tai-stack-3 {
|
|
1778
|
+
gap: var(--tai-space-3);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
.tai-stack-6 {
|
|
1782
|
+
gap: var(--tai-space-6);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
.tai-row {
|
|
1786
|
+
display: flex;
|
|
1787
|
+
align-items: center;
|
|
1788
|
+
gap: var(--tai-space-3);
|
|
1789
|
+
flex-wrap: wrap;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/* A flex item's default `min-width: auto` refuses to shrink below its own
|
|
1793
|
+
content, so server text in a row — an origin, a failure message — pushes the
|
|
1794
|
+
row wider than its container instead of wrapping inside it. */
|
|
1795
|
+
.tai-row > .tai-muted {
|
|
1796
|
+
min-width: 0;
|
|
1797
|
+
overflow-wrap: anywhere;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
.tai-page-header {
|
|
1801
|
+
display: flex;
|
|
1802
|
+
align-items: flex-start;
|
|
1803
|
+
justify-content: space-between;
|
|
1804
|
+
gap: var(--tai-space-4);
|
|
1805
|
+
flex-wrap: wrap;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
.tai-page-title {
|
|
1809
|
+
margin: 0;
|
|
1810
|
+
font-size: var(--tai-text-xl);
|
|
1811
|
+
font-weight: 700;
|
|
1812
|
+
line-height: 1.2;
|
|
1813
|
+
color: var(--tai-color-heading);
|
|
1814
|
+
/* A flex item of `.tai-page-header`, holding a caller's own string — a
|
|
1815
|
+
plugin's page name, a server id. Measured at 320-640 px, a 54-character
|
|
1816
|
+
unbroken title took the document to a scroll width of 734 px; the pair
|
|
1817
|
+
lets the title shrink inside the header and break inside the word. */
|
|
1818
|
+
min-width: 0;
|
|
1819
|
+
overflow-wrap: anywhere;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/* The rest of the caller-supplied prose: a plugin's card or section heading, a
|
|
1823
|
+
dialog's own title, an empty state's headline, a page's description. None of
|
|
1824
|
+
it is a string this repo owns, so an unbroken run breaks inside the word
|
|
1825
|
+
rather than overflowing its block and pushing the document — the same rule
|
|
1826
|
+
`.tai-error-state` states for arbitrary server text. `break-word` and not
|
|
1827
|
+
`anywhere`: these are blocks, so what they need is a break when the word
|
|
1828
|
+
does not fit the line, and `anywhere` would additionally collapse their
|
|
1829
|
+
min-content, which only a flex item such as `.tai-page-title` needs. */
|
|
1830
|
+
.tai-card-title,
|
|
1831
|
+
.tai-dialog-title,
|
|
1832
|
+
.tai-empty-state-title,
|
|
1833
|
+
.tai-page-description,
|
|
1834
|
+
.tai-section-title {
|
|
1835
|
+
overflow-wrap: break-word;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
.tai-page-description {
|
|
1839
|
+
margin: var(--tai-space-2) 0 0;
|
|
1840
|
+
max-width: 60ch;
|
|
1841
|
+
font-family: var(--tai-font-sans);
|
|
1842
|
+
font-size: var(--tai-text-md);
|
|
1843
|
+
line-height: 1.5;
|
|
1844
|
+
color: var(--tai-color-text-muted);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
.tai-page-actions {
|
|
1848
|
+
display: flex;
|
|
1849
|
+
align-items: center;
|
|
1850
|
+
gap: var(--tai-space-2);
|
|
1851
|
+
flex-wrap: wrap;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
.tai-section-title {
|
|
1855
|
+
margin: 0;
|
|
1856
|
+
font-size: var(--tai-text-section);
|
|
1857
|
+
font-weight: 700;
|
|
1858
|
+
line-height: 1.25;
|
|
1859
|
+
color: var(--tai-color-heading);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
.tai-kpi {
|
|
1863
|
+
font-size: var(--tai-text-display);
|
|
1864
|
+
font-weight: 700;
|
|
1865
|
+
line-height: 1.1;
|
|
1866
|
+
font-variant-numeric: tabular-nums;
|
|
1867
|
+
color: var(--tai-color-heading);
|
|
1868
|
+
letter-spacing: -0.03em;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
.tai-muted {
|
|
1872
|
+
color: var(--tai-color-text-muted);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
.tai-mono {
|
|
1876
|
+
font-family: var(--tai-font-mono);
|
|
1877
|
+
font-variant-numeric: tabular-nums;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/* Visually hidden but still announced — the accessible name behind an
|
|
1881
|
+
icon-only segment or control. */
|
|
1882
|
+
.tai-visually-hidden {
|
|
1883
|
+
position: absolute;
|
|
1884
|
+
width: 1px;
|
|
1885
|
+
height: 1px;
|
|
1886
|
+
margin: -1px;
|
|
1887
|
+
padding: 0;
|
|
1888
|
+
overflow: hidden;
|
|
1889
|
+
clip-path: inset(50%);
|
|
1890
|
+
white-space: nowrap;
|
|
1891
|
+
border: 0;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/* -- Shell -----------------------------------------------------------------------
|
|
1895
|
+
The application frame: a fixed-width sidebar beside a scrolling main
|
|
1896
|
+
column at >=1024, a 72 px labelled icon rail from 640, and a sticky top bar
|
|
1897
|
+
below 640.
|
|
1898
|
+
|
|
1899
|
+
The bar's height is named once. Three unlinked rules have to agree on it —
|
|
1900
|
+
the bar's own box, the scroll padding that stops it covering an element
|
|
1901
|
+
scrolled to by a focus move, and the scroll margin that stops it covering an
|
|
1902
|
+
anchor jump — and as three bare literals a change to one of them left the
|
|
1903
|
+
other two behind with every gate green. It is NOT a `--tai-*` token: the
|
|
1904
|
+
published contract is `tokens.css`, and this is private to this sheet, so it
|
|
1905
|
+
carries a namespace of its own exactly as that file's own gate requires. */
|
|
1906
|
+
:root {
|
|
1907
|
+
--shell-topbar-height: 56px;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
.tai-shell {
|
|
1911
|
+
display: grid;
|
|
1912
|
+
grid-template-columns: 232px minmax(0, 1fr);
|
|
1913
|
+
min-height: 100%;
|
|
1914
|
+
background: var(--tai-color-bg);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
.tai-shell-sidebar {
|
|
1918
|
+
box-sizing: border-box;
|
|
1919
|
+
display: flex;
|
|
1920
|
+
flex-direction: column;
|
|
1921
|
+
gap: var(--tai-space-2);
|
|
1922
|
+
/* A full-viewport sticky column in a TOUCH band, where `vh` is the LARGE
|
|
1923
|
+
viewport: on a phone with a retracting browser chrome the sidebar is taller
|
|
1924
|
+
than the screen by the height of that chrome, and its last nav item sits
|
|
1925
|
+
under it until the user scrolls. `dvh` is the live value. The pair is the
|
|
1926
|
+
fallback, not a preference — `dvh` arrived after this package's floor
|
|
1927
|
+
(Chrome 108, Firefox 101), and a browser below it drops the second
|
|
1928
|
+
declaration and keeps the first. */
|
|
1929
|
+
height: 100vh;
|
|
1930
|
+
height: 100dvh;
|
|
1931
|
+
position: sticky;
|
|
1932
|
+
top: 0;
|
|
1933
|
+
overflow-y: auto;
|
|
1934
|
+
padding: var(--tai-space-4) var(--tai-space-3);
|
|
1935
|
+
background: var(--tai-color-surface);
|
|
1936
|
+
border-right: 1px solid var(--tai-color-border);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
.tai-shell-main {
|
|
1940
|
+
min-width: 0;
|
|
1941
|
+
background: var(--tai-color-bg);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
.tai-brand {
|
|
1945
|
+
display: flex;
|
|
1946
|
+
align-items: center;
|
|
1947
|
+
gap: var(--tai-space-2);
|
|
1948
|
+
padding: var(--tai-space-2) var(--tai-space-3);
|
|
1949
|
+
font-size: var(--tai-text-lg);
|
|
1950
|
+
font-weight: 700;
|
|
1951
|
+
letter-spacing: -0.03em;
|
|
1952
|
+
color: var(--tai-color-heading);
|
|
1953
|
+
text-decoration: none;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
.tai-brand-mark {
|
|
1957
|
+
width: 24px;
|
|
1958
|
+
height: 24px;
|
|
1959
|
+
border-radius: var(--tai-radius-tile);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
.tai-brand-label {
|
|
1963
|
+
white-space: nowrap;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
.tai-topbar {
|
|
1967
|
+
display: none;
|
|
1968
|
+
position: sticky;
|
|
1969
|
+
top: 0;
|
|
1970
|
+
z-index: var(--tai-z-sticky);
|
|
1971
|
+
box-sizing: border-box;
|
|
1972
|
+
align-items: center;
|
|
1973
|
+
justify-content: space-between;
|
|
1974
|
+
gap: var(--tai-space-2);
|
|
1975
|
+
height: var(--shell-topbar-height);
|
|
1976
|
+
padding: 0 var(--tai-space-3);
|
|
1977
|
+
background: var(--tai-color-surface);
|
|
1978
|
+
border-bottom: 1px solid var(--tai-color-border);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/* -- Master / detail ---------------------------------------------------------------
|
|
1982
|
+
Two fluid panes side by side; below 1024 exactly ONE pane is on screen and
|
|
1983
|
+
the consumer drives which through `data-pane`. */
|
|
1984
|
+
.tai-split {
|
|
1985
|
+
display: grid;
|
|
1986
|
+
grid-template-columns: minmax(0, 1fr) minmax(480px, 1.4fr);
|
|
1987
|
+
gap: var(--tai-space-6);
|
|
1988
|
+
align-items: start;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.tai-split-list,
|
|
1992
|
+
.tai-split-detail {
|
|
1993
|
+
min-width: 0;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
/* -- Card grids ------------------------------------------------------------------- */
|
|
1997
|
+
.tai-grid-cards {
|
|
1998
|
+
display: grid;
|
|
1999
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
2000
|
+
gap: var(--tai-space-4);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
/* -- Responsive bands ---------------------------------------------------------------
|
|
2004
|
+
1024-1279 medium: the card grid drops to two columns. The sidebar keeps the
|
|
2005
|
+
width it has at every band above 1023, and `.tai-split` is already fluid —
|
|
2006
|
+
its `minmax()` columns hold the detail pane at 480 px or wider. */
|
|
2007
|
+
@media (max-width: 1279px) {
|
|
2008
|
+
.tai-grid-cards {
|
|
2009
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/* 640-1023 compact: the sidebar collapses to a 72 px icon rail that KEEPS a
|
|
2014
|
+
short text label under each icon — this is a touch band, never
|
|
2015
|
+
tooltip-only — and only one master/detail pane is on screen. */
|
|
2016
|
+
@media (max-width: 1023px) {
|
|
2017
|
+
.tai-shell {
|
|
2018
|
+
grid-template-columns: 72px minmax(0, 1fr);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
.tai-shell-sidebar {
|
|
2022
|
+
padding: var(--tai-space-3) var(--tai-space-1);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/* SCOPED TO THE RAIL. These three groups only make sense inside the 72 px
|
|
2026
|
+
icon rail the block above builds, so each is a DESCENDANT of
|
|
2027
|
+
`.tai-shell-sidebar`. Unscoped, they reached every `.tai-nav-link` and
|
|
2028
|
+
`.tai-nav-section-header` on the page — including a shell that lays its
|
|
2029
|
+
sidebar out itself and never becomes a rail — and column-stacked its nav
|
|
2030
|
+
labels at 11 px inside a full-width sidebar while hiding every section
|
|
2031
|
+
heading, which is the visible regression the scope prevents. The heading
|
|
2032
|
+
still NAMES its list either way: `aria-labelledby` computes its text from
|
|
2033
|
+
the target's subtree whether or not that target is rendered, so a
|
|
2034
|
+
`display: none` header is a name without a visible label rather than no
|
|
2035
|
+
name — a reason to keep the scope, not the reason. */
|
|
2036
|
+
.tai-shell-sidebar .tai-nav-item,
|
|
2037
|
+
.tai-shell-sidebar .tai-nav-link {
|
|
2038
|
+
flex-direction: column;
|
|
2039
|
+
gap: var(--tai-space-1);
|
|
2040
|
+
padding: var(--tai-space-2) var(--tai-space-1);
|
|
2041
|
+
font-size: var(--tai-text-xs);
|
|
2042
|
+
line-height: 1.2;
|
|
2043
|
+
text-align: center;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
.tai-shell-sidebar .tai-nav-item[aria-current='page'],
|
|
2047
|
+
.tai-shell-sidebar .tai-nav-link[aria-current='page'] {
|
|
2048
|
+
box-shadow: inset 0 -3px 0 0 var(--tai-color-accent);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
.tai-shell-sidebar .tai-brand-label,
|
|
2052
|
+
.tai-shell-sidebar .tai-nav-section-header {
|
|
2053
|
+
display: none;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
.tai-split {
|
|
2057
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
.tai-split[data-pane='list'] > .tai-split-detail,
|
|
2061
|
+
.tai-split[data-pane='detail'] > .tai-split-list {
|
|
2062
|
+
display: none;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
.tai-page {
|
|
2066
|
+
padding: var(--tai-space-5) var(--tai-space-6);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/* <640 phone: the rail gives way to a sticky 56 px top bar; the shell, the card
|
|
2071
|
+
grid and the page header each fall to a single column, and the page tightens
|
|
2072
|
+
its padding and its gap. */
|
|
2073
|
+
@media (max-width: 639px) {
|
|
2074
|
+
.tai-shell {
|
|
2075
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
.tai-shell-sidebar {
|
|
2079
|
+
display: none;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
.tai-topbar {
|
|
2083
|
+
display: flex;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
.tai-page {
|
|
2087
|
+
padding: var(--tai-space-4);
|
|
2088
|
+
gap: var(--tai-space-5);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
.tai-grid-cards {
|
|
2092
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
.tai-page-header {
|
|
2096
|
+
flex-direction: column;
|
|
2097
|
+
align-items: stretch;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
/* The sticky top bar would otherwise cover an element scrolled to by a
|
|
2102
|
+
keyboard focus move or an in-page anchor (WCAG 2.2 — 2.4.11). Padding
|
|
2103
|
+
covers focus scrolling, margin covers anchor jumps. */
|
|
2104
|
+
@media (max-width: 639px) {
|
|
2105
|
+
html {
|
|
2106
|
+
scroll-padding-top: var(--shell-topbar-height);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/* Scoped to the jump targets: the margin and the scrollport's padding are
|
|
2110
|
+
ADDITIVE, so putting it on every element with an id would push a focused
|
|
2111
|
+
control twice the bar's height down the page. */
|
|
2112
|
+
:target,
|
|
2113
|
+
#main-content {
|
|
2114
|
+
scroll-margin-top: var(--shell-topbar-height);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
/* Touch: the pointer-target minimum wins over the 36 px control height. */
|
|
2119
|
+
@media (pointer: coarse) {
|
|
2120
|
+
.tai-btn,
|
|
2121
|
+
.tai-tab,
|
|
2122
|
+
.tai-nav-item,
|
|
2123
|
+
.tai-nav-link,
|
|
2124
|
+
.tai-choice,
|
|
2125
|
+
/* The pointer-target minimum is for TARGETS. `.tai-chip-static` is a tag or a
|
|
2126
|
+
"+N more" count, not a control (see its rule above), so inflating it to
|
|
2127
|
+
44 px buys nothing and pushes read-only content around. */
|
|
2128
|
+
.tai-chip:not(.tai-chip-static) {
|
|
2129
|
+
min-height: var(--tai-control-height-coarse);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
.tai-icon-btn {
|
|
2133
|
+
width: var(--tai-control-height-coarse);
|
|
2134
|
+
height: var(--tai-control-height-coarse);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
.tai-input,
|
|
2138
|
+
.tai-select-trigger {
|
|
2139
|
+
min-height: var(--tai-control-height-coarse);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
.tai-select-item,
|
|
2143
|
+
.tai-skip-link {
|
|
2144
|
+
min-height: var(--tai-control-height-coarse);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
.tai-segment {
|
|
2148
|
+
min-width: var(--tai-control-height-coarse);
|
|
2149
|
+
min-height: var(--tai-control-height-coarse);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/* 16 px box + 14 px above and below = the 44 px touch target. The overlay
|
|
2153
|
+
grows VERTICALLY only: a horizontal group sets its options a spacing step
|
|
2154
|
+
apart, and 14 px sideways would reach across that gap into the previous
|
|
2155
|
+
option's label. `.tai-choice`'s own 44 px covers a labelled box. */
|
|
2156
|
+
.tai-checkbox::after,
|
|
2157
|
+
.tai-radio::after {
|
|
2158
|
+
inset-block: -14px;
|
|
2159
|
+
inset-inline: -4px;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/* Reduced motion: nothing animates, nothing lifts. The spinner and the
|
|
2164
|
+
progress bar keep their accessible names, so those two states are still
|
|
2165
|
+
announced without movement. (The skeleton is `aria-hidden` and names
|
|
2166
|
+
nothing either way — it is a visual placeholder only, which is why the rule
|
|
2167
|
+
below keeps it a visible block rather than relying on it being announced.) */
|
|
2168
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2169
|
+
.tai-skeleton::after,
|
|
2170
|
+
.tai-spinner,
|
|
2171
|
+
.tai-progress-fill-indeterminate {
|
|
2172
|
+
animation: none;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/* The sweep is what made the block read as pending. Stopping the overlay
|
|
2176
|
+
where it stands would freeze the band across the block, so the overlay
|
|
2177
|
+
goes entirely: what is left is the flat line tone the band rides over, so
|
|
2178
|
+
the block is exactly the moving skeleton with the band taken away — still
|
|
2179
|
+
visibly THERE, and at the same contrast, without moving. */
|
|
2180
|
+
.tai-skeleton::after {
|
|
2181
|
+
display: none;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/* Without the sweep, a short fill parked at one end says nothing, so the
|
|
2185
|
+
accent fills the whole track: work is in flight, position unknown. The bar
|
|
2186
|
+
carries no `aria-valuenow`, so it is not read as complete. */
|
|
2187
|
+
.tai-progress-fill-indeterminate {
|
|
2188
|
+
width: 100%;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
.tai-btn,
|
|
2192
|
+
.tai-icon-btn,
|
|
2193
|
+
.tai-card,
|
|
2194
|
+
.tai-chip,
|
|
2195
|
+
.tai-tab,
|
|
2196
|
+
.tai-nav-item,
|
|
2197
|
+
.tai-nav-link,
|
|
2198
|
+
.tai-segment,
|
|
2199
|
+
.tai-input,
|
|
2200
|
+
.tai-textarea,
|
|
2201
|
+
.tai-select-trigger,
|
|
2202
|
+
.tai-skip-link {
|
|
2203
|
+
transition: none;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
.tai-btn:hover:not(:disabled):not([aria-disabled='true']),
|
|
2207
|
+
.tai-card-interactive:hover,
|
|
2208
|
+
.tai-card-interactive:focus-within {
|
|
2209
|
+
transform: none;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|