Compare commits
8 Commits
240401b9cb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
912e209b80 | ||
|
|
f123db6faf | ||
|
|
8edc0df755 | ||
|
|
8691ee0f7f | ||
|
|
0e437d2b88 | ||
|
|
bcc6727917 | ||
|
|
af52f32330 | ||
|
|
d278e8998a |
@@ -56,8 +56,10 @@ def generate_bicorder_text(json_data):
|
|||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
# First pass: calculate maximum widths for left and right terms
|
# First pass: calculate maximum widths for left and right terms
|
||||||
|
# Also collect all terms for the glossary
|
||||||
max_left_width = 0
|
max_left_width = 0
|
||||||
max_right_width = 0
|
max_right_width = 0
|
||||||
|
glossary_terms = {} # Dictionary to store term: description mappings
|
||||||
|
|
||||||
# Check diagnostic gradients
|
# Check diagnostic gradients
|
||||||
for diagnostic_set in json_data.get("diagnostic", []):
|
for diagnostic_set in json_data.get("diagnostic", []):
|
||||||
@@ -67,6 +69,12 @@ def generate_bicorder_text(json_data):
|
|||||||
max_left_width = max(max_left_width, len(term_left))
|
max_left_width = max(max_left_width, len(term_left))
|
||||||
max_right_width = max(max_right_width, len(term_right))
|
max_right_width = max(max_right_width, len(term_right))
|
||||||
|
|
||||||
|
# Collect terms for glossary
|
||||||
|
if term_left:
|
||||||
|
glossary_terms[term_left] = gradient.get("term_left_description", "")
|
||||||
|
if term_right:
|
||||||
|
glossary_terms[term_right] = gradient.get("term_right_description", "")
|
||||||
|
|
||||||
# Check analysis items
|
# Check analysis items
|
||||||
for analysis_item in json_data.get("analysis", []):
|
for analysis_item in json_data.get("analysis", []):
|
||||||
term_left = analysis_item.get("term_left", "")
|
term_left = analysis_item.get("term_left", "")
|
||||||
@@ -74,6 +82,12 @@ def generate_bicorder_text(json_data):
|
|||||||
max_left_width = max(max_left_width, len(term_left))
|
max_left_width = max(max_left_width, len(term_left))
|
||||||
max_right_width = max(max_right_width, len(term_right))
|
max_right_width = max(max_right_width, len(term_right))
|
||||||
|
|
||||||
|
# Collect terms for glossary
|
||||||
|
if term_left:
|
||||||
|
glossary_terms[term_left] = analysis_item.get("term_left_description", "")
|
||||||
|
if term_right:
|
||||||
|
glossary_terms[term_right] = analysis_item.get("term_right_description", "")
|
||||||
|
|
||||||
# Calculate the width needed for centering
|
# Calculate the width needed for centering
|
||||||
# Gradient line format: "{left_term} < [|||||||||] > {right_term}"
|
# Gradient line format: "{left_term} < [|||||||||] > {right_term}"
|
||||||
# That's: max_left_width + 3 + 11 + 3 + max_right_width
|
# That's: max_left_width + 3 + 11 + 3 + max_right_width
|
||||||
@@ -136,6 +150,31 @@ def generate_bicorder_text(json_data):
|
|||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
|
# Glossary section
|
||||||
|
lines.append(center_text("GLOSSARY", center_width))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Generate pandoc-compatible table
|
||||||
|
# Sort terms alphabetically (case-insensitive) for consistent output
|
||||||
|
sorted_terms = sorted(glossary_terms.items(), key=lambda x: x[0].lower())
|
||||||
|
|
||||||
|
if sorted_terms:
|
||||||
|
# Calculate column widths for the table
|
||||||
|
max_term_width = max(len(term) for term, _ in sorted_terms)
|
||||||
|
max_term_width = max(max_term_width, len("Term")) # At least as wide as header
|
||||||
|
|
||||||
|
# Build the table
|
||||||
|
# Header row
|
||||||
|
lines.append(f"| {'Term'.ljust(max_term_width)} | Description |")
|
||||||
|
# Separator row
|
||||||
|
lines.append(f"| {'-' * max_term_width} | {'-' * 11} |")
|
||||||
|
|
||||||
|
# Data rows
|
||||||
|
for term, description in sorted_terms:
|
||||||
|
lines.append(f"| {term.ljust(max_term_width)} | {description} |")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="apple-touch-icon" href="/icon-192.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||||
<meta name="description" content="A diagnostic tool for the study of protocols" />
|
<meta name="description" content="A diagnostic tool for the study of protocols" />
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
<meta name="theme-color" content="#1a1a2e" />
|
||||||
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
|
|
||||||
<title>Protocol Bicorder</title>
|
<title>Protocol Bicorder</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 B After Width: | Height: | Size: 5.3 KiB |
14
bicorder-app/public/favicon.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#1a1a2e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#16213e;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="128" height="128" fill="url(#bgGrad)" rx="16"/>
|
||||||
|
|
||||||
|
<!-- Large hash symbol -->
|
||||||
|
<text x="64" y="95" font-family="'Courier New', monospace" font-size="100" font-weight="bold" fill="#bbe1fa" text-anchor="middle">#</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 577 B |
BIN
bicorder-app/public/icon-192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
bicorder-app/public/icon-512.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
bicorder-app/public/icon-maskable-192.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
bicorder-app/public/icon-maskable-512.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
47
bicorder-app/public/icon-maskable.svg
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#1a1a2e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#16213e;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="accentGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#0f4c75;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#3282b8;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Full bleed background for maskable icon -->
|
||||||
|
<rect width="512" height="512" fill="url(#bgGradient)"/>
|
||||||
|
|
||||||
|
<!-- Outer frame - scaled to fit safe zone (80% diameter = 40% radius from center) -->
|
||||||
|
<rect x="102" y="102" width="308" height="308" fill="none" stroke="url(#accentGradient)" stroke-width="6" rx="15"/>
|
||||||
|
|
||||||
|
<!-- Bicorder pattern "|#|" scaled for safe zone -->
|
||||||
|
<g font-family="'Courier New', monospace" font-weight="bold" text-anchor="middle">
|
||||||
|
<!-- Left bar -->
|
||||||
|
<text x="200" y="290" font-size="140" fill="#3282b8">|</text>
|
||||||
|
|
||||||
|
<!-- Center hash -->
|
||||||
|
<text x="256" y="290" font-size="140" fill="#bbe1fa">#</text>
|
||||||
|
|
||||||
|
<!-- Right bar -->
|
||||||
|
<text x="312" y="290" font-size="140" fill="#3282b8">|</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Decorative scan lines -->
|
||||||
|
<line x1="130" y1="160" x2="382" y2="160" stroke="#3282b8" stroke-width="2" opacity="0.3"/>
|
||||||
|
<line x1="130" y1="352" x2="382" y2="352" stroke="#3282b8" stroke-width="2" opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- Corner accents -->
|
||||||
|
<rect x="115" y="115" width="25" height="3" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
<rect x="115" y="115" width="3" height="25" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
|
||||||
|
<rect x="372" y="115" width="25" height="3" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
<rect x="394" y="115" width="3" height="25" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
|
||||||
|
<rect x="115" y="394" width="25" height="3" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
<rect x="115" y="372" width="3" height="25" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
|
||||||
|
<rect x="372" y="394" width="25" height="3" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
<rect x="394" y="372" width="3" height="25" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -1,11 +1,47 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
<rect width="100" height="100" fill="#ffffff"/>
|
<defs>
|
||||||
<text x="50" y="35" font-family="monospace" font-size="16" fill="#000000" text-anchor="middle">
|
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
Protocol
|
<stop offset="0%" style="stop-color:#1a1a2e;stop-opacity:1" />
|
||||||
</text>
|
<stop offset="100%" style="stop-color:#16213e;stop-opacity:1" />
|
||||||
<text x="50" y="55" font-family="monospace" font-size="20" fill="#000000" text-anchor="middle" font-weight="bold">
|
</linearGradient>
|
||||||
BICORDER
|
<linearGradient id="accentGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
</text>
|
<stop offset="0%" style="stop-color:#0f4c75;stop-opacity:1" />
|
||||||
<rect x="20" y="65" width="60" height="10" fill="none" stroke="#000000" stroke-width="2"/>
|
<stop offset="100%" style="stop-color:#3282b8;stop-opacity:1" />
|
||||||
<rect x="22" y="67" width="20" height="6" fill="#000000"/>
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="512" height="512" fill="url(#bgGradient)" rx="80"/>
|
||||||
|
|
||||||
|
<!-- Outer frame -->
|
||||||
|
<rect x="64" y="64" width="384" height="384" fill="none" stroke="url(#accentGradient)" stroke-width="8" rx="20"/>
|
||||||
|
|
||||||
|
<!-- Bicorder pattern "|#|" in monospace style -->
|
||||||
|
<g font-family="'Courier New', monospace" font-weight="bold" text-anchor="middle">
|
||||||
|
<!-- Left bar -->
|
||||||
|
<text x="180" y="300" font-size="180" fill="#3282b8">|</text>
|
||||||
|
|
||||||
|
<!-- Center hash -->
|
||||||
|
<text x="256" y="300" font-size="180" fill="#bbe1fa">#</text>
|
||||||
|
|
||||||
|
<!-- Right bar -->
|
||||||
|
<text x="332" y="300" font-size="180" fill="#3282b8">|</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Decorative scan lines -->
|
||||||
|
<line x1="96" y1="140" x2="416" y2="140" stroke="#3282b8" stroke-width="2" opacity="0.3"/>
|
||||||
|
<line x1="96" y1="372" x2="416" y2="372" stroke="#3282b8" stroke-width="2" opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- Corner accents -->
|
||||||
|
<rect x="80" y="80" width="30" height="4" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
<rect x="80" y="80" width="4" height="30" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
|
||||||
|
<rect x="402" y="80" width="30" height="4" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
<rect x="428" y="80" width="4" height="30" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
|
||||||
|
<rect x="80" y="428" width="30" height="4" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
<rect x="80" y="402" width="4" height="30" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
|
||||||
|
<rect x="402" y="428" width="30" height="4" fill="#bbe1fa" opacity="0.8"/>
|
||||||
|
<rect x="428" y="402" width="4" height="30" fill="#bbe1fa" opacity="0.8"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 535 B After Width: | Height: | Size: 2.0 KiB |
@@ -1,250 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Protocol Bicorder",
|
|
||||||
"schema": "bicorder.schema.json",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"description": "A diagnostic tool for the study of protocols",
|
|
||||||
"author": "Nathan Schneider",
|
|
||||||
"date_modified": "2025-11-21",
|
|
||||||
"metadata": {
|
|
||||||
"protocol": "IB Learner Profile",
|
|
||||||
"analyst": "Kids at the school",
|
|
||||||
"standpoint": "Students at the school",
|
|
||||||
"timestamp": "2025-11-24T03:16:26.468Z"
|
|
||||||
},
|
|
||||||
"diagnostic": [
|
|
||||||
{
|
|
||||||
"set_name": "Design",
|
|
||||||
"set_description": "How the protocol is created and remembered",
|
|
||||||
"gradients": [
|
|
||||||
{
|
|
||||||
"term_left": "explicit",
|
|
||||||
"term_left_description": "The design is stated explicitly somewhere that is accessible to participants",
|
|
||||||
"term_right": "implicit",
|
|
||||||
"term_right_description": "The design is not stated explicitly but is learned by participants in another way",
|
|
||||||
"value": 2,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "precise",
|
|
||||||
"term_left_description": "The design is specified with a high level of precision that eliminates ambiguity in implementation",
|
|
||||||
"term_right": "interpretive",
|
|
||||||
"term_right_description": "The design is ambiguous, allowing participants a wide range of interpretation",
|
|
||||||
"value": 4,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "institutional",
|
|
||||||
"term_left_description": "Design occurs through processes that involve powerful institutions and widespread recognition as normative",
|
|
||||||
"term_right": "vernacular",
|
|
||||||
"term_right_description": "Design occurs through evolving, peer-to-peer community interactions in order to suit participant-defined goals",
|
|
||||||
"value": 1,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "documenting",
|
|
||||||
"term_left_description": "The primary purpose is to document or validate activity that is occurring",
|
|
||||||
"term_right": "enabling",
|
|
||||||
"term_right_description": "The primary purpose is to enable activity that might not happen otherwise",
|
|
||||||
"value": 5,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "static",
|
|
||||||
"term_left_description": "Designed to be as fixed and unchanging as possible",
|
|
||||||
"term_right": "malleable",
|
|
||||||
"term_right_description": "Designed to be changed by participants according to evolving needs",
|
|
||||||
"value": 1,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "technical",
|
|
||||||
"term_left_description": "Primarily concerned with interactions among technologies",
|
|
||||||
"term_right": "social",
|
|
||||||
"term_right_description": "Primarily concerned with interactions among people or groups",
|
|
||||||
"value": 9,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "universal",
|
|
||||||
"term_left_description": "Addressed to a global audience",
|
|
||||||
"term_right": "particular",
|
|
||||||
"term_right_description": "Addressed to a specific community",
|
|
||||||
"value": 5,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "durable",
|
|
||||||
"term_left_description": "Designed to be persistently available",
|
|
||||||
"term_right": "ephemeral",
|
|
||||||
"term_right_description": "Designed to vanish when no longer needed",
|
|
||||||
"value": 1,
|
|
||||||
"notes": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_name": "Entanglement",
|
|
||||||
"set_description": "How the protocol relates with participant agents",
|
|
||||||
"gradients": [
|
|
||||||
{
|
|
||||||
"term_left": "macro",
|
|
||||||
"term_left_description": "Operates at large scales involving many participants or broad scope",
|
|
||||||
"term_right": "micro",
|
|
||||||
"term_right_description": "Operates at small scales with few participants or narrow scope",
|
|
||||||
"value": 3,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "sovereign",
|
|
||||||
"term_left_description": "A distinctive operating logic, not subject to any other entity",
|
|
||||||
"term_right": "subsidiary",
|
|
||||||
"term_right_description": "An operating logic under the control of a particular entity",
|
|
||||||
"value": 8,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "self-enforcing",
|
|
||||||
"term_left_description": "Rules are automatically enforced through its own mechanisms",
|
|
||||||
"term_right": "enforced",
|
|
||||||
"term_right_description": "Rules require external enforcement by authorities or institutions",
|
|
||||||
"value": 7,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "abstract",
|
|
||||||
"term_left_description": "Participants learn the protocol by studying it intellectually",
|
|
||||||
"term_right": "embodied",
|
|
||||||
"term_right_description": "Participants learn the protocol by physically practicing it",
|
|
||||||
"value": 2,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "obligatory",
|
|
||||||
"term_left_description": "Participation is compulsory for a certain class of agents",
|
|
||||||
"term_right": "voluntary",
|
|
||||||
"term_right_description": "Participation in the protocol is optional and not coerced",
|
|
||||||
"value": 2,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "flocking",
|
|
||||||
"term_left_description": "Coordination occurs through centralized direction or direct mimicry",
|
|
||||||
"term_right": "swarming",
|
|
||||||
"term_right_description": "Coordination occurs through distributed interactions without central direction",
|
|
||||||
"value": 8,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "defensible",
|
|
||||||
"term_left_description": "Strong boundaries and protections against external influence",
|
|
||||||
"term_right": "exposed",
|
|
||||||
"term_right_description": "Weak boundaries and vulnerable to external influence",
|
|
||||||
"value": null,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "exclusive",
|
|
||||||
"term_left_description": "Excludes the use of other protocols that might be available to adopt",
|
|
||||||
"term_right": "non-exclusive",
|
|
||||||
"term_right_description": "Does not exclude the use of any other protocols",
|
|
||||||
"value": 8,
|
|
||||||
"notes": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"set_name": "Experience",
|
|
||||||
"set_description": "How the protocol is perceived in the context of its implementation",
|
|
||||||
"gradients": [
|
|
||||||
{
|
|
||||||
"term_left": "sufficient",
|
|
||||||
"term_left_description": "Adequately meets the needs and goals of participants",
|
|
||||||
"term_right": "insufficient",
|
|
||||||
"term_right_description": "Does not, on its own, adequately meet the needs and goals of participants",
|
|
||||||
"value": 8,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "crystallized",
|
|
||||||
"term_left_description": "Content and meaning are settled and widely agreed upon",
|
|
||||||
"term_right": "contested",
|
|
||||||
"term_right_description": "Content and meaning are disputed or under debate",
|
|
||||||
"value": 1,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "trust-evading",
|
|
||||||
"term_left_description": "Minimizes the need for trust among participants",
|
|
||||||
"term_right": "trust-inducing",
|
|
||||||
"term_right_description": "Relies on or cultivates trust among participants",
|
|
||||||
"value": 9,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "predictable",
|
|
||||||
"term_left_description": "Produces expected and consistent outcomes",
|
|
||||||
"term_right": "emergent",
|
|
||||||
"term_right_description": "Produces unexpected or novel outcomes",
|
|
||||||
"value": 9,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "exclusion",
|
|
||||||
"term_left_description": "The protocol creates barriers or excludes certain participants",
|
|
||||||
"term_right": "inclusion",
|
|
||||||
"term_right_description": "The protocol reduces barriers and includes diverse participants",
|
|
||||||
"value": 9,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "Kafka",
|
|
||||||
"term_left_description": "Fosters experiences of absurd complexity, alienation, and powerlessness",
|
|
||||||
"term_right": "Whitehead",
|
|
||||||
"term_right_description": "Enables participants to carry out desired activities with less work or thought",
|
|
||||||
"value": 6,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "dead",
|
|
||||||
"term_left_description": "Not actively utilized by relevant participants",
|
|
||||||
"term_right": "alive",
|
|
||||||
"term_right_description": "Actively utilized by relevant participants",
|
|
||||||
"value": 8,
|
|
||||||
"notes": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"analysis": [
|
|
||||||
{
|
|
||||||
"term_left": "hardness",
|
|
||||||
"term_left_description": "The protocol tends toward properties characterized by hardness",
|
|
||||||
"term_right": "softness",
|
|
||||||
"term_right_description": "The protocol tends toward properties characterized by softness",
|
|
||||||
"instructions": "Take all the 'value' fields in the gradients above and determine a mean. Round it to the nearest integer. That is the 'value' here.",
|
|
||||||
"automated": true,
|
|
||||||
"value": 5,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "polarized",
|
|
||||||
"term_left_description": "The analyst tended toward more extreme high or low readings",
|
|
||||||
"term_right": "centrist",
|
|
||||||
"term_right_description": "The analyst tended toward readings at the middle of the gradients",
|
|
||||||
"instructions": "Take all the 'value' fields in the gradients above. Assess their degree of polarization. For instance, if all the values are either 1 or 9, the output would be 1, and if all of them are 5, the output would be 9.",
|
|
||||||
"automated": true,
|
|
||||||
"value": 3,
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"term_left": "not useful",
|
|
||||||
"term_left_description": "The bicorder was not useful or relevant for analyzing this protocol",
|
|
||||||
"term_right": "very useful",
|
|
||||||
"term_right_description": "The bicorder was very useful and relevant for analyzing this protocol",
|
|
||||||
"instructions": "Evaluate the usefulness of this bicorder as a tool for analyzing this protocol, considering whether the gradient terms seemed revealing or irrelevant.",
|
|
||||||
"automated": false,
|
|
||||||
"value": 7,
|
|
||||||
"notes": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
import MetadataFields from './components/MetadataFields.svelte';
|
import MetadataFields from './components/MetadataFields.svelte';
|
||||||
import AnalysisDisplay from './components/AnalysisDisplay.svelte';
|
import AnalysisDisplay from './components/AnalysisDisplay.svelte';
|
||||||
import ExportControls from './components/ExportControls.svelte';
|
import ExportControls from './components/ExportControls.svelte';
|
||||||
|
import HelpModal from './components/HelpModal.svelte';
|
||||||
|
|
||||||
// Load bicorder data from build-time constant
|
// Load bicorder data from build-time constant
|
||||||
let data: BicorderState = JSON.parse(JSON.stringify(__BICORDER_DATA__));
|
let data: BicorderState = JSON.parse(JSON.stringify(__BICORDER_DATA__));
|
||||||
@@ -14,6 +15,87 @@
|
|||||||
data.metadata.timestamp = new Date().toISOString();
|
data.metadata.timestamp = new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// View mode and navigation state
|
||||||
|
type ViewMode = 'focused' | 'list';
|
||||||
|
let viewMode: ViewMode = 'focused'; // Focused is default
|
||||||
|
let currentScreen = 0;
|
||||||
|
let refreshKey = 0; // Used to force component refresh in focused mode
|
||||||
|
let isHelpOpen = false;
|
||||||
|
|
||||||
|
// Screen types
|
||||||
|
type Screen =
|
||||||
|
| { type: 'metadata' }
|
||||||
|
| { type: 'gradient'; setIndex: number; gradientIndex: number; gradient: Gradient; setName: string }
|
||||||
|
| { type: 'analysis'; index: number; gradient: AnalysisGradient }
|
||||||
|
| { type: 'export' };
|
||||||
|
|
||||||
|
// Calculate all screens based on current shortform setting
|
||||||
|
function calculateScreens(): Screen[] {
|
||||||
|
const screens: Screen[] = [];
|
||||||
|
|
||||||
|
// Metadata screen
|
||||||
|
screens.push({ type: 'metadata' });
|
||||||
|
|
||||||
|
// Diagnostic gradient screens
|
||||||
|
data.diagnostic.forEach((diagnosticSet, setIndex) => {
|
||||||
|
diagnosticSet.gradients.forEach((gradient, gradientIndex) => {
|
||||||
|
if (!data.metadata.shortform || gradient.shortform) {
|
||||||
|
screens.push({
|
||||||
|
type: 'gradient',
|
||||||
|
setIndex,
|
||||||
|
gradientIndex,
|
||||||
|
gradient,
|
||||||
|
setName: diagnosticSet.set_name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analysis screens (not in shortform)
|
||||||
|
if (!data.metadata.shortform) {
|
||||||
|
data.analysis.forEach((gradient, index) => {
|
||||||
|
screens.push({ type: 'analysis', index, gradient });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export screen
|
||||||
|
screens.push({ type: 'export' });
|
||||||
|
|
||||||
|
return screens;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: screens = calculateScreens();
|
||||||
|
$: currentScreenData = screens[currentScreen];
|
||||||
|
$: totalScreens = screens.length;
|
||||||
|
|
||||||
|
function goToNextScreen() {
|
||||||
|
if (currentScreen < totalScreens - 1) {
|
||||||
|
currentScreen++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPrevScreen() {
|
||||||
|
if (currentScreen > 0) {
|
||||||
|
currentScreen--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleViewMode() {
|
||||||
|
viewMode = viewMode === 'focused' ? 'list' : 'focused';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ASCII progress bar
|
||||||
|
function generateProgressBar(current: number, total: number): string {
|
||||||
|
const filled = '#';
|
||||||
|
const empty = '-';
|
||||||
|
const barLength = Math.min(total, 20); // Cap at 20 characters for display
|
||||||
|
const filledCount = Math.round((current / total) * barLength);
|
||||||
|
const emptyCount = barLength - filledCount;
|
||||||
|
return filled.repeat(filledCount) + empty.repeat(emptyCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: progressBar = generateProgressBar(currentScreen + 1, totalScreens);
|
||||||
|
|
||||||
// Load saved state from localStorage
|
// Load saved state from localStorage
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const saved = localStorage.getItem('bicorder-state');
|
const saved = localStorage.getItem('bicorder-state');
|
||||||
@@ -45,6 +127,27 @@
|
|||||||
console.error('Failed to load saved state:', e);
|
console.error('Failed to load saved state:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keyboard navigation for focused mode
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (viewMode !== 'focused') return;
|
||||||
|
|
||||||
|
// Only navigate if not typing in an input
|
||||||
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
goToNextScreen();
|
||||||
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
goToPrevScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save state whenever it changes
|
// Save state whenever it changes
|
||||||
@@ -111,12 +214,28 @@
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openHelp() {
|
||||||
|
isHelpOpen = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<HelpModal bind:isOpen={isHelpOpen} />
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
<button class="help-btn" on:click={openHelp} aria-label="About the Bicorder">?</button>
|
||||||
<div class="title">Protocol</div>
|
<div class="title">Protocol</div>
|
||||||
<div class="title">BICORDER</div>
|
<div class="title">BICORDER</div>
|
||||||
|
<button class="mode-toggle" on:click={toggleViewMode} aria-label="Toggle view mode">
|
||||||
|
{viewMode === 'focused' ? '☰' : '⊡'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if viewMode === 'list'}
|
||||||
|
<!-- LIST MODE: Show all sections -->
|
||||||
|
<div class="description">
|
||||||
|
<p>A diagnostic tool for the study of protocols</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MetadataFields
|
<MetadataFields
|
||||||
@@ -168,6 +287,135 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<ExportControls {data} on:reset={handleReset} />
|
<ExportControls {data} on:reset={handleReset} />
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<!-- FOCUSED MODE: Show one screen at a time -->
|
||||||
|
{#if currentScreen === 0}
|
||||||
|
<div class="description">
|
||||||
|
<p>A diagnostic tool for the study of protocols</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="focused-container">
|
||||||
|
{#if currentScreenData.type === 'metadata'}
|
||||||
|
<div class="focused-screen">
|
||||||
|
<MetadataFields
|
||||||
|
metadata={data.metadata}
|
||||||
|
on:update={handleMetadataUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else if currentScreenData.type === 'gradient'}
|
||||||
|
{@const screen = currentScreenData}
|
||||||
|
<div class="focused-screen gradient-screen">
|
||||||
|
<div class="screen-category">{screen.setName.toUpperCase()}</div>
|
||||||
|
|
||||||
|
<div class="gradient-focused">
|
||||||
|
<div class="term-desc left-desc">
|
||||||
|
<div class="term-name">{screen.gradient.term_left}</div>
|
||||||
|
<div class="description-text">{screen.gradient.term_left_description}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#key refreshKey}
|
||||||
|
<GradientSlider
|
||||||
|
gradient={screen.gradient}
|
||||||
|
focusedMode={true}
|
||||||
|
on:change={(e) => {
|
||||||
|
data.diagnostic[screen.setIndex].gradients[screen.gradientIndex].value = e.detail ?? null;
|
||||||
|
data = data;
|
||||||
|
refreshKey++; // Force component refresh
|
||||||
|
}}
|
||||||
|
on:notes={(e) => {
|
||||||
|
data.diagnostic[screen.setIndex].gradients[screen.gradientIndex].notes = e.detail;
|
||||||
|
data = data;
|
||||||
|
refreshKey++; // Force component refresh
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<div class="term-desc right-desc">
|
||||||
|
<div class="term-name">{screen.gradient.term_right}</div>
|
||||||
|
<div class="description-text">{screen.gradient.term_right_description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else if currentScreenData.type === 'analysis'}
|
||||||
|
{@const screen = currentScreenData}
|
||||||
|
<div class="focused-screen gradient-screen">
|
||||||
|
<div class="screen-category">ANALYSIS</div>
|
||||||
|
|
||||||
|
<div class="gradient-focused">
|
||||||
|
<div class="term-desc left-desc">
|
||||||
|
<div class="term-name">{screen.gradient.term_left}</div>
|
||||||
|
<div class="description-text">{screen.gradient.term_left_description}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#key refreshKey}
|
||||||
|
<AnalysisDisplay
|
||||||
|
gradient={screen.gradient}
|
||||||
|
focusedMode={true}
|
||||||
|
on:change={(e) => {
|
||||||
|
if (!screen.gradient.automated) {
|
||||||
|
data.analysis[screen.index].value = e.detail ?? null;
|
||||||
|
data = data;
|
||||||
|
refreshKey++; // Force component refresh
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:notes={(e) => {
|
||||||
|
data.analysis[screen.index].notes = e.detail;
|
||||||
|
data = data;
|
||||||
|
refreshKey++; // Force component refresh
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<div class="term-desc right-desc">
|
||||||
|
<div class="term-name">{screen.gradient.term_right}</div>
|
||||||
|
<div class="description-text">{screen.gradient.term_right_description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else if currentScreenData.type === 'export'}
|
||||||
|
<div class="focused-screen">
|
||||||
|
<ExportControls {data} focusedMode={true} on:reset={handleReset} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation and Progress -->
|
||||||
|
<div class="focused-navigation" class:no-border={currentScreenData.type === 'export'}>
|
||||||
|
<div class="nav-buttons">
|
||||||
|
<button
|
||||||
|
class="nav-btn prev-btn"
|
||||||
|
on:click={goToPrevScreen}
|
||||||
|
disabled={currentScreen === 0}
|
||||||
|
aria-label="Previous screen"
|
||||||
|
>
|
||||||
|
← Previous
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="nav-btn next-btn"
|
||||||
|
on:click={goToNextScreen}
|
||||||
|
disabled={currentScreen === totalScreens - 1}
|
||||||
|
aria-label="Next screen"
|
||||||
|
>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-indicator">
|
||||||
|
<div class="progress-bar">{progressBar}</div>
|
||||||
|
<div class="progress-numbers">{currentScreen + 1} / {totalScreens}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>Initiated by <a href="https://nathanschneider.info" rel="nofollow">Nathan Schneider</a>; <a href="https://git.medlab.host/ntnsndr/protocol-bicorder/src/branch/main/bicorder-app" rel="nofollow">source code</a> licensed under the <a href="https://firstdonoharm.dev/" rel="nofollow">Hippocratic License</a> (do no harm!).</p>
|
||||||
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -175,13 +423,17 @@
|
|||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
border-bottom: 2px solid var(--border-color);
|
border-bottom: 2px solid var(--border-color);
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -190,6 +442,80 @@
|
|||||||
letter-spacing: 0.2rem;
|
letter-spacing: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.help-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--fg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity 0.2s, background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: var(--input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--fg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
min-height: auto;
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: var(--input-bg);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--input-bg);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 2rem 1rem 1rem;
|
||||||
|
border-top: 2px solid var(--border-color);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.diagnostic-set, .analysis-section {
|
.diagnostic-set, .analysis-section {
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
@@ -211,6 +537,133 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Focused Mode Styles */
|
||||||
|
.focused-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused-screen {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-screen {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-category {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-focused {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-desc {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-desc.left-desc {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-desc.right-desc {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0.8;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused-navigation {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
margin-top: auto;
|
||||||
|
border-top: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused-navigation.no-border {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-indicator {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: 0.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-numbers {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
main {
|
main {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
@@ -223,5 +676,51 @@
|
|||||||
.set-header {
|
.set-header {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.help-btn {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 1.5rem 0.5rem 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-numbers {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused-container {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-focused {
|
||||||
|
padding: 0;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
min-width: 80px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import Tooltip from './Tooltip.svelte';
|
import Tooltip from './Tooltip.svelte';
|
||||||
|
|
||||||
export let gradient: AnalysisGradient;
|
export let gradient: AnalysisGradient;
|
||||||
|
export let focusedMode: boolean = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
change: number | null;
|
change: number | null;
|
||||||
@@ -98,12 +99,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="analysis-gradient">
|
<div class="analysis-gradient">
|
||||||
<div class="gradient-row">
|
<div class="gradient-row" class:focused={focusedMode}>
|
||||||
|
{#if !focusedMode}
|
||||||
<div class="term left">
|
<div class="term left">
|
||||||
<Tooltip text={gradient.term_left_description}>
|
<Tooltip text={gradient.term_left_description}>
|
||||||
{gradient.term_left}
|
{gradient.term_left}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="bar-container">
|
<div class="bar-container">
|
||||||
<button
|
<button
|
||||||
@@ -124,11 +127,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if !focusedMode}
|
||||||
<div class="term right">
|
<div class="term right">
|
||||||
<Tooltip text={gradient.term_right_description}>
|
<Tooltip text={gradient.term_right_description}>
|
||||||
{gradient.term_right}
|
{gradient.term_right}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="value-display">
|
<div class="value-display">
|
||||||
@@ -200,6 +205,15 @@
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gradient-row.focused {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-row.focused .bar-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.term {
|
.term {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -257,6 +271,11 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.analysis-gradient .value-display {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.auto-label {
|
.auto-label {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import type { BicorderState } from '../types';
|
import type { BicorderState } from '../types';
|
||||||
|
|
||||||
export let data: BicorderState;
|
export let data: BicorderState;
|
||||||
|
export let focusedMode: boolean = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
reset: void;
|
reset: void;
|
||||||
@@ -121,7 +122,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="export-controls">
|
<section class="export-controls" class:focused={focusedMode}>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button on:click={exportToJSON}>
|
<button on:click={exportToJSON}>
|
||||||
💾 Export JSON
|
💾 Export JSON
|
||||||
@@ -170,6 +171,10 @@
|
|||||||
border-top: 2px solid var(--border-color);
|
border-top: 2px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-controls.focused {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import Tooltip from './Tooltip.svelte';
|
import Tooltip from './Tooltip.svelte';
|
||||||
|
|
||||||
export let gradient: Gradient;
|
export let gradient: Gradient;
|
||||||
|
export let focusedMode: boolean = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
change: number;
|
change: number;
|
||||||
@@ -94,12 +95,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="gradient-container">
|
<div class="gradient-container">
|
||||||
<div class="gradient-row">
|
<div class="gradient-row" class:focused={focusedMode}>
|
||||||
|
{#if !focusedMode}
|
||||||
<div class="term left">
|
<div class="term left">
|
||||||
<Tooltip text={gradient.term_left_description}>
|
<Tooltip text={gradient.term_left_description}>
|
||||||
{gradient.term_left}
|
{gradient.term_left}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="bar-container">
|
<div class="bar-container">
|
||||||
<button
|
<button
|
||||||
@@ -117,11 +120,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if !focusedMode}
|
||||||
<div class="term right">
|
<div class="term right">
|
||||||
<Tooltip text={gradient.term_right_description}>
|
<Tooltip text={gradient.term_right_description}>
|
||||||
{gradient.term_right}
|
{gradient.term_right}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="value-display">
|
<div class="value-display">
|
||||||
@@ -179,6 +184,15 @@
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gradient-row.focused {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-row.focused .bar-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.term {
|
.term {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -231,6 +245,11 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gradient-container .value-display {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
146
bicorder-app/src/components/HelpModal.svelte
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let isOpen = false;
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div class="modal-backdrop" on:click={closeModal} on:keydown={() => {}} role="button" tabindex="-1">
|
||||||
|
<div class="modal-content" on:click|stopPropagation on:keydown={() => {}} role="dialog" aria-modal="true">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Help & Instructions</h2>
|
||||||
|
<button class="close-btn" on:click={closeModal} aria-label="Close help">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>The Protocol Bicorder is a diagnostic tool for the study of protocols. It allows a human or machine user to evaluate protocol characteristics along a series of gradients between opposing terms.</p>
|
||||||
|
<p>The name is a tribute to the tricorder, a fictional device in the Star Trek universe that the characters can use to obtain all manner of empirical data about their surroundings.</p>
|
||||||
|
<p>To carry out the diagnostic, the analyst should consider the protocol from the perspective of one of the <code>gradients</code> at a time. The gradients invite the analyst to determine where the protocol lies between two terms.</p>
|
||||||
|
<p>This is inevitably an interpretive exercise, but do your best to identify the most accurate <code>value</code>, with <code>1</code> being closest to <code>term_left</code> and <code>9</code> being closest to <code>term_right</code>.</p>
|
||||||
|
<p>Choosing a <code>value</code> in the middle, such as <code>5</code>, can mean "a bit of both." Leaving the gradient <code>value</code> as <code>null</code> means "not applicable."</p>
|
||||||
|
<p>There is a <code>notes</code> field for the analyst to add additional context or explanation.</p>
|
||||||
|
<p>The <code>shortform</code> option allows the use of an abbreviated version of the bicorder, including only the most salient gradients.</p>
|
||||||
|
<p>Happy protocol watching!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
letter-spacing: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--fg-color);
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body h3 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body code {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body strong {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modal-content {
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
|
||||||
export let text: string;
|
export let text: string;
|
||||||
|
|
||||||
let showTooltip = false;
|
let showTooltip = false;
|
||||||
let touchTimer: number;
|
let touchTimer: number;
|
||||||
let isLongPress = false;
|
let isLongPress = false;
|
||||||
|
let tooltipElement: HTMLSpanElement;
|
||||||
|
let wrapperElement: HTMLSpanElement;
|
||||||
|
let tooltipPosition = { align: 'center', vertical: 'top' };
|
||||||
|
|
||||||
function handleTouchStart() {
|
async function handleTouchStart() {
|
||||||
isLongPress = false;
|
isLongPress = false;
|
||||||
touchTimer = window.setTimeout(() => {
|
touchTimer = window.setTimeout(async () => {
|
||||||
showTooltip = true;
|
showTooltip = true;
|
||||||
isLongPress = true;
|
isLongPress = true;
|
||||||
|
await tick();
|
||||||
|
await updateTooltipPosition();
|
||||||
}, 500); // Show after 500ms press
|
}, 500); // Show after 500ms press
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,12 +30,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick() {
|
async function handleClick() {
|
||||||
// Toggle tooltip on click/tap
|
// Toggle tooltip on click/tap
|
||||||
if (!isLongPress) {
|
if (!isLongPress) {
|
||||||
showTooltip = !showTooltip;
|
showTooltip = !showTooltip;
|
||||||
// Auto-hide after 3 seconds
|
// Auto-hide after 3 seconds
|
||||||
if (showTooltip) {
|
if (showTooltip) {
|
||||||
|
await tick();
|
||||||
|
await updateTooltipPosition();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showTooltip = false;
|
showTooltip = false;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
@@ -37,16 +46,59 @@
|
|||||||
isLongPress = false;
|
isLongPress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseEnter() {
|
async function handleMouseEnter() {
|
||||||
showTooltip = true;
|
showTooltip = true;
|
||||||
|
await tick();
|
||||||
|
await updateTooltipPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseLeave() {
|
function handleMouseLeave() {
|
||||||
showTooltip = false;
|
showTooltip = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateTooltipPosition() {
|
||||||
|
// Wait for next frame to ensure rendering is complete
|
||||||
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
|
|
||||||
|
if (!tooltipElement || !wrapperElement) return;
|
||||||
|
|
||||||
|
const wrapperRect = wrapperElement.getBoundingClientRect();
|
||||||
|
const tooltipRect = tooltipElement.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const padding = 10;
|
||||||
|
|
||||||
|
// Calculate tooltip width and position
|
||||||
|
const tooltipWidth = tooltipRect.width;
|
||||||
|
const wrapperCenterX = wrapperRect.left + wrapperRect.width / 2;
|
||||||
|
|
||||||
|
// Determine horizontal alignment
|
||||||
|
let align = 'center';
|
||||||
|
|
||||||
|
// Check if center alignment would overflow
|
||||||
|
const centerLeft = wrapperCenterX - tooltipWidth / 2;
|
||||||
|
const centerRight = wrapperCenterX + tooltipWidth / 2;
|
||||||
|
|
||||||
|
if (centerLeft < padding) {
|
||||||
|
// Would overflow left, align to left edge of wrapper
|
||||||
|
align = 'left';
|
||||||
|
} else if (centerRight > viewportWidth - padding) {
|
||||||
|
// Would overflow right, align to right edge of wrapper
|
||||||
|
align = 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check vertical overflow
|
||||||
|
let vertical = 'top';
|
||||||
|
if (tooltipRect.top < padding) {
|
||||||
|
vertical = 'bottom';
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipPosition = { align, vertical };
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
|
bind:this={wrapperElement}
|
||||||
class="tooltip-wrapper"
|
class="tooltip-wrapper"
|
||||||
on:mouseenter={handleMouseEnter}
|
on:mouseenter={handleMouseEnter}
|
||||||
on:mouseleave={handleMouseLeave}
|
on:mouseleave={handleMouseLeave}
|
||||||
@@ -61,7 +113,16 @@
|
|||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
{#if showTooltip}
|
{#if showTooltip}
|
||||||
<span class="tooltip-text" role="status" aria-live="polite">
|
<span
|
||||||
|
bind:this={tooltipElement}
|
||||||
|
class="tooltip-text"
|
||||||
|
class:align-left={tooltipPosition.align === 'left'}
|
||||||
|
class:align-right={tooltipPosition.align === 'right'}
|
||||||
|
class:align-center={tooltipPosition.align === 'center'}
|
||||||
|
class:position-bottom={tooltipPosition.vertical === 'bottom'}
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -79,19 +140,43 @@
|
|||||||
.tooltip-text {
|
.tooltip-text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 125%;
|
bottom: 125%;
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
border: 2px solid var(--border-color);
|
border: 2px solid var(--border-color);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: 300px;
|
max-width: min(300px, calc(100vw - 20px));
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal alignment variants */
|
||||||
|
.tooltip-text.align-center {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-text.align-left {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-text.align-right {
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical position variant (when tooltip would overflow top) */
|
||||||
|
.tooltip-text.position-bottom {
|
||||||
|
bottom: auto;
|
||||||
|
top: 125%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -100,7 +185,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-text::after {
|
/* Arrow positioning for center-aligned tooltip */
|
||||||
|
.tooltip-text.align-center::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
@@ -111,9 +197,48 @@
|
|||||||
border-color: var(--border-color) transparent transparent transparent;
|
border-color: var(--border-color) transparent transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-text.align-center.position-bottom::after {
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
border-color: transparent transparent var(--border-color) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Arrow positioning for left-aligned tooltip */
|
||||||
|
.tooltip-text.align-left::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 1rem;
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--border-color) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-text.align-left.position-bottom::after {
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
border-color: transparent transparent var(--border-color) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Arrow positioning for right-aligned tooltip */
|
||||||
|
.tooltip-text.align-right::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 1rem;
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--border-color) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-text.align-right.position-bottom::after {
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
border-color: transparent transparent var(--border-color) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.tooltip-text {
|
.tooltip-text {
|
||||||
max-width: 250px;
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,39 @@ export default defineConfig({
|
|||||||
svelte(),
|
svelte(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
includeAssets: ['favicon.ico'],
|
includeAssets: ['favicon.ico', 'favicon.svg', 'icon-192.png', 'icon-512.png', 'icon-maskable-192.png', 'icon-maskable-512.png'],
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Protocol Bicorder',
|
name: 'Protocol Bicorder',
|
||||||
short_name: 'Bicorder',
|
short_name: 'Bicorder',
|
||||||
description: 'A diagnostic tool for the study of protocols',
|
description: 'A diagnostic tool for the study of protocols',
|
||||||
theme_color: '#ffffff',
|
theme_color: '#1a1a2e',
|
||||||
background_color: '#ffffff',
|
background_color: '#1a1a2e',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
icons: [
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/icon-192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icon-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icon-maskable-192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icon-maskable-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
src: '/icon.svg',
|
src: '/icon.svg',
|
||||||
sizes: 'any',
|
sizes: 'any',
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Protocol Bicorder",
|
"name": "Protocol Bicorder",
|
||||||
"schema": "bicorder.schema.json",
|
"schema": "bicorder.schema.json",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"description": "A diagnostic tool for the study of protocols",
|
"description": "A diagnostic tool for the study of protocols",
|
||||||
"author": "Nathan Schneider",
|
"author": "Nathan Schneider",
|
||||||
"date_modified": "2025-11-21",
|
"date_modified": "2025-12-02",
|
||||||
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"protocol": null,
|
"protocol": null,
|
||||||
@@ -161,10 +161,10 @@
|
|||||||
"shortform": false
|
"shortform": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"term_left": "exclusive",
|
"term_left": "monopolistic",
|
||||||
"term_left_description": "Excludes the use of other protocols that might be available to adopt",
|
"term_left_description": "Excludes the use of other protocols that might be available to adopt",
|
||||||
"term_right": "non-exclusive",
|
"term_right": "pluralistic",
|
||||||
"term_right_description": "Does not exclude the use of any other protocols",
|
"term_right_description": "Interoperates with other protocols and does not exclude their use",
|
||||||
"value": null,
|
"value": null,
|
||||||
"notes": null,
|
"notes": null,
|
||||||
"shortform": false
|
"shortform": false
|
||||||
|
|||||||
62
bicorder.txt
@@ -9,7 +9,7 @@
|
|||||||
DESIGN
|
DESIGN
|
||||||
explicit < [|||||||||] > implicit
|
explicit < [|||||||||] > implicit
|
||||||
precise < [|||||||||] > interpretive
|
precise < [|||||||||] > interpretive
|
||||||
elite < [|||||||||] > vernacular
|
institutional < [|||||||||] > vernacular
|
||||||
documenting < [|||||||||] > enabling
|
documenting < [|||||||||] > enabling
|
||||||
static < [|||||||||] > malleable
|
static < [|||||||||] > malleable
|
||||||
technical < [|||||||||] > social
|
technical < [|||||||||] > social
|
||||||
@@ -24,7 +24,7 @@ self-enforcing < [|||||||||] > enforced
|
|||||||
obligatory < [|||||||||] > voluntary
|
obligatory < [|||||||||] > voluntary
|
||||||
flocking < [|||||||||] > swarming
|
flocking < [|||||||||] > swarming
|
||||||
defensible < [|||||||||] > exposed
|
defensible < [|||||||||] > exposed
|
||||||
exclusive < [|||||||||] > non-exclusive
|
monopolistic < [|||||||||] > pluralistic
|
||||||
|
|
||||||
EXPERIENCE
|
EXPERIENCE
|
||||||
sufficient < [|||||||||] > insufficient
|
sufficient < [|||||||||] > insufficient
|
||||||
@@ -38,3 +38,61 @@ self-enforcing < [|||||||||] > enforced
|
|||||||
ANALYSIS
|
ANALYSIS
|
||||||
hardness < [|||||||||] > softness
|
hardness < [|||||||||] > softness
|
||||||
polarized < [|||||||||] > centrist
|
polarized < [|||||||||] > centrist
|
||||||
|
not useful < [|||||||||] > very useful
|
||||||
|
|
||||||
|
GLOSSARY
|
||||||
|
|
||||||
|
| Term | Description |
|
||||||
|
| -------------- | ----------- |
|
||||||
|
| abstract | Participants learn the protocol by studying it intellectually |
|
||||||
|
| alive | Actively utilized by relevant participants |
|
||||||
|
| centrist | The analyst tended toward readings at the middle of the gradients |
|
||||||
|
| contested | Content and meaning are disputed or under debate |
|
||||||
|
| crystallized | Content and meaning are settled and widely agreed upon |
|
||||||
|
| dead | Not actively utilized by relevant participants |
|
||||||
|
| defensible | Strong boundaries and protections against external influence |
|
||||||
|
| documenting | The primary purpose is to document or validate activity that is occurring |
|
||||||
|
| durable | Designed to be persistently available |
|
||||||
|
| embodied | Participants learn the protocol by physically practicing it |
|
||||||
|
| emergent | Produces unexpected or novel outcomes |
|
||||||
|
| enabling | The primary purpose is to enable activity that might not happen otherwise |
|
||||||
|
| enforced | Rules require external enforcement by authorities or institutions |
|
||||||
|
| ephemeral | Designed to vanish when no longer needed |
|
||||||
|
| exclusion | The protocol creates barriers or excludes certain participants |
|
||||||
|
| explicit | The design is stated explicitly somewhere that is accessible to participants |
|
||||||
|
| exposed | Weak boundaries and vulnerable to external influence |
|
||||||
|
| flocking | Coordination occurs through centralized direction or direct mimicry |
|
||||||
|
| hardness | The protocol tends toward properties characterized by hardness |
|
||||||
|
| implicit | The design is not stated explicitly but is learned by participants in another way |
|
||||||
|
| inclusion | The protocol reduces barriers and includes diverse participants |
|
||||||
|
| institutional | Design occurs through processes that involve powerful institutions and widespread recognition as normative |
|
||||||
|
| insufficient | Does not, on its own, adequately meet the needs and goals of participants |
|
||||||
|
| interpretive | The design is ambiguous, allowing participants a wide range of interpretation |
|
||||||
|
| Kafka | Fosters experiences of absurd complexity, alienation, and powerlessness |
|
||||||
|
| macro | Operates at large scales involving many participants or broad scope |
|
||||||
|
| malleable | Designed to be changed by participants according to evolving needs |
|
||||||
|
| micro | Operates at small scales with few participants or narrow scope |
|
||||||
|
| monopolistic | Excludes the use of other protocols that might be available to adopt |
|
||||||
|
| not useful | The bicorder was not useful or relevant for analyzing this protocol |
|
||||||
|
| obligatory | Participation is compulsory for a certain class of agents |
|
||||||
|
| particular | Addressed to a specific community |
|
||||||
|
| pluralistic | Interoperates with other protocols and does not exclude their use |
|
||||||
|
| polarized | The analyst tended toward more extreme high or low readings |
|
||||||
|
| precise | The design is specified with a high level of precision that eliminates ambiguity in implementation |
|
||||||
|
| predictable | Produces expected and consistent outcomes |
|
||||||
|
| self-enforcing | Rules are automatically enforced through its own mechanisms |
|
||||||
|
| social | Primarily concerned with interactions among people or groups |
|
||||||
|
| softness | The protocol tends toward properties characterized by softness |
|
||||||
|
| sovereign | A distinctive operating logic, not subject to any other entity |
|
||||||
|
| static | Designed to be as fixed and unchanging as possible |
|
||||||
|
| subsidiary | An operating logic under the control of a particular entity |
|
||||||
|
| sufficient | Adequately meets the needs and goals of participants |
|
||||||
|
| swarming | Coordination occurs through distributed interactions without central direction |
|
||||||
|
| technical | Primarily concerned with interactions among technologies |
|
||||||
|
| trust-evading | Minimizes the need for trust among participants |
|
||||||
|
| trust-inducing | Relies on or cultivates trust among participants |
|
||||||
|
| universal | Addressed to a global audience |
|
||||||
|
| vernacular | Design occurs through evolving, peer-to-peer community interactions in order to suit participant-defined goals |
|
||||||
|
| very useful | The bicorder was very useful and relevant for analyzing this protocol |
|
||||||
|
| voluntary | Participation in the protocol is optional and not coerced |
|
||||||
|
| Whitehead | Enables participants to carry out desired activities with less work or thought |
|
||||||
|
|||||||